mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-10-28 16:13:32 +03:00
Compare commits
3734 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b792fd461d | ||
|
|
6d0b2121b8 | ||
|
|
7d861eb92f | ||
|
|
f14544992d | ||
|
|
78aa114d21 | ||
|
|
97e646fda4 | ||
|
|
2d25109aa3 | ||
|
|
afece6e85c | ||
|
|
03151d72a0 | ||
|
|
08bc097dd4 | ||
|
|
28fc6f5e0f | ||
|
|
f455aea5ff | ||
|
|
8f645388fc | ||
|
|
7bce091177 | ||
|
|
eb47ce41a3 | ||
|
|
49f94a5650 | ||
|
|
870df2d861 | ||
|
|
343ef172d1 | ||
|
|
bca01170a4 | ||
|
|
ddf28c299c | ||
|
|
b5c1be34c2 | ||
|
|
0204ca48ee | ||
|
|
0358fdf971 | ||
|
|
a0e093d527 | ||
|
|
19de853729 | ||
|
|
66725386b0 | ||
|
|
e1e94c815d | ||
|
|
7c432e392b | ||
|
|
37fcdbe534 | ||
|
|
c9f7d27551 | ||
|
|
843b69bfa8 | ||
|
|
61ebcb6ed4 | ||
|
|
ceb9f68ded | ||
|
|
d7e7250b16 | ||
|
|
0a8e37ccc8 | ||
|
|
2cff632b2d | ||
|
|
2c658b7c31 | ||
|
|
4997853b78 | ||
|
|
6fba843d15 | ||
|
|
3659552fac | ||
|
|
3432795930 | ||
|
|
fb4a247916 | ||
|
|
a174bf7ba0 | ||
|
|
77be04d427 | ||
|
|
cf185b9ab5 | ||
|
|
30bb5c934d | ||
|
|
b9397e6e20 | ||
|
|
fd6beedc0c | ||
|
|
45b94153c1 | ||
|
|
02e96a9339 | ||
|
|
b3edb5fb0a | ||
|
|
58d553b1a2 | ||
|
|
2f26779f4a | ||
|
|
9f0e6d946a | ||
|
|
85b142f559 | ||
|
|
1be5abd122 | ||
|
|
a9ca71bae1 | ||
|
|
0805f71461 | ||
|
|
abb83e48b0 | ||
|
|
f52249b77b | ||
|
|
1bbd7f635a | ||
|
|
8a8304e81a | ||
|
|
e7a491b0f5 | ||
|
|
6c9d3edc18 | ||
|
|
5dca6d4f65 | ||
|
|
2af0af173e | ||
|
|
7fdc77ce5f | ||
|
|
86dc155d1c | ||
|
|
5de44c77c2 | ||
|
|
dfc9420385 | ||
|
|
e66f325aa3 | ||
|
|
aad651f2a1 | ||
|
|
67a68915db | ||
|
|
71287022ca | ||
|
|
3da27c3bc8 | ||
|
|
020927cc56 | ||
|
|
8667af700d | ||
|
|
4e65e7ee84 | ||
|
|
aca14765c9 | ||
|
|
be91e4eb80 | ||
|
|
24d7dee9b9 | ||
|
|
6ef8a9a391 | ||
|
|
08ddc323f9 | ||
|
|
279cc6406e | ||
|
|
3dd6c85c87 | ||
|
|
e4ecda0e61 | ||
|
|
640c567210 | ||
|
|
8a2d8e86a5 | ||
|
|
6a7cbe070c | ||
|
|
1a0c9872b2 | ||
|
|
863f719da3 | ||
|
|
68588287ef | ||
|
|
60ee74c68a | ||
|
|
ed35c8e91e | ||
|
|
f757d58534 | ||
|
|
c154d23cfe | ||
|
|
c1f1faca58 | ||
|
|
dc0191aaf1 | ||
|
|
a8117a9d03 | ||
|
|
f2648a54c0 | ||
|
|
1dc26f11d2 | ||
|
|
c203d15b17 | ||
|
|
2c54ac9be3 | ||
|
|
2c73a955e2 | ||
|
|
658b97f5b4 | ||
|
|
ba496919dd | ||
|
|
24bb509b96 | ||
|
|
56409dcbb4 | ||
|
|
b4a6068d5b | ||
|
|
40cf2b950e | ||
|
|
7397a95a7c | ||
|
|
9dd7c021fd | ||
|
|
9f6d2e4cb7 | ||
|
|
93e3c431ec | ||
|
|
a9b89fd7b3 | ||
|
|
da96945f39 | ||
|
|
0bc9950b42 | ||
|
|
c3660bee57 | ||
|
|
dea7f4d1d8 | ||
|
|
00128e837f | ||
|
|
d9eaad24ad | ||
|
|
9565559213 | ||
|
|
1c9c9f0d6b | ||
|
|
d3b120bbe7 | ||
|
|
a024d34f6d | ||
|
|
33fbaa9598 | ||
|
|
86da00eb50 | ||
|
|
45c8f71af0 | ||
|
|
70aebb4981 | ||
|
|
0233c83719 | ||
|
|
e37546b795 | ||
|
|
2741762199 | ||
|
|
ecbd13f099 | ||
|
|
5bba3dac82 | ||
|
|
ed4b2dd4e1 | ||
|
|
fad9d5e482 | ||
|
|
1660c62aa3 | ||
|
|
dddf360db6 | ||
|
|
7268dafffd | ||
|
|
8bf4727555 | ||
|
|
208b1fab62 | ||
|
|
149cf5a120 | ||
|
|
5973d71e2f | ||
|
|
486acde2e3 | ||
|
|
e16d0b52ae | ||
|
|
c0348893af | ||
|
|
0ffd97a183 | ||
|
|
2143051eaf | ||
|
|
7eaa065d76 | ||
|
|
7a465606bc | ||
|
|
cd3398db20 | ||
|
|
18da535643 | ||
|
|
d301b93b0d | ||
|
|
66553a36da | ||
|
|
95af4f60ea | ||
|
|
3ee978484a | ||
|
|
8b5547104d | ||
|
|
0d5d403fb8 | ||
|
|
91ff43e6ab | ||
|
|
bc6a2b67c0 | ||
|
|
4cd5774257 | ||
|
|
c744b83480 | ||
|
|
3d55bdaf5a | ||
|
|
2809e75307 | ||
|
|
a83c3aacf8 | ||
|
|
8645b0ab1e | ||
|
|
490d808cdb | ||
|
|
f12de9a3fd | ||
|
|
1b4bf3b8c3 | ||
|
|
64e780e500 | ||
|
|
709d67226c | ||
|
|
cab3b2b78a | ||
|
|
c50edb826b | ||
|
|
332f9a79e3 | ||
|
|
534b71e5a4 | ||
|
|
f0b3d798bc | ||
|
|
b01072e2e3 | ||
|
|
bce9758338 | ||
|
|
3a589e240c | ||
|
|
6bdd509b48 | ||
|
|
4f6783a991 | ||
|
|
012c1102f9 | ||
|
|
78547e8c68 | ||
|
|
fdc1a1c924 | ||
|
|
f877b849eb | ||
|
|
a1f890e3b4 | ||
|
|
7aa87da449 | ||
|
|
64f74d0511 | ||
|
|
5d679b5be5 | ||
|
|
6bc658b5e0 | ||
|
|
bc6de6c336 | ||
|
|
583528c448 | ||
|
|
6307470ac0 | ||
|
|
35914c0406 | ||
|
|
5861124c2a | ||
|
|
0ef2987911 | ||
|
|
3d970e4706 | ||
|
|
838e73fa94 | ||
|
|
42c98d42e0 | ||
|
|
055c0d3bf4 | ||
|
|
16106a28d5 | ||
|
|
9eb7f38485 | ||
|
|
82ab4c4c6d | ||
|
|
e345550c7f | ||
|
|
904a353bff | ||
|
|
cb9e1b0705 | ||
|
|
ececfae31a | ||
|
|
156481583d | ||
|
|
b86743de24 | ||
|
|
e7ecf53b4a | ||
|
|
48b0519cb4 | ||
|
|
0a4cea5e4c | ||
|
|
6f2f40b983 | ||
|
|
28a8e3c157 | ||
|
|
ba85eda7f0 | ||
|
|
ef724b4b4a | ||
|
|
03d99f19c9 | ||
|
|
f41ee21dba | ||
|
|
d6ef5ff70a | ||
|
|
0520a5421a | ||
|
|
907a9cefec | ||
|
|
fafaaf21b1 | ||
|
|
115f8c8374 | ||
|
|
3f8f17a62e | ||
|
|
d3de9fae6b | ||
|
|
0ecd0523f3 | ||
|
|
68045f50f6 | ||
|
|
b3d6c33257 | ||
|
|
f7f8d2d542 | ||
|
|
584728bc89 | ||
|
|
4b802c0cb3 | ||
|
|
f43c2c92b5 | ||
|
|
beb418080f | ||
|
|
9b5eaf4b5f | ||
|
|
126208166d | ||
|
|
dc7eed4c80 | ||
|
|
198cc852c3 | ||
|
|
3931c11255 | ||
|
|
85ac404722 | ||
|
|
6b03820338 | ||
|
|
c14bbab945 | ||
|
|
932525566f | ||
|
|
1b0dd48cdb | ||
|
|
3893849d21 | ||
|
|
92a6027aa0 | ||
|
|
4022d39f76 | ||
|
|
61c2eefec1 | ||
|
|
257d9201f6 | ||
|
|
c773b852a1 | ||
|
|
0a3a5f6d59 | ||
|
|
ce3596f299 | ||
|
|
279cad0385 | ||
|
|
d80fe8bc73 | ||
|
|
109a110202 | ||
|
|
93cc623891 | ||
|
|
04f3133f27 | ||
|
|
f7b1981bcc | ||
|
|
3576c22401 | ||
|
|
88a66d64f1 | ||
|
|
7ba8de13e4 | ||
|
|
5c45cf3465 | ||
|
|
d3ecfdc71e | ||
|
|
56db74986f | ||
|
|
a4299c5f1c | ||
|
|
2341dd9f62 | ||
|
|
5056ecf493 | ||
|
|
16cc17af00 | ||
|
|
e3c53f8044 | ||
|
|
5f1dd7f150 | ||
|
|
2f0732df81 | ||
|
|
74c7948e72 | ||
|
|
33b509c0a6 | ||
|
|
365392eefe | ||
|
|
32da821c17 | ||
|
|
b5ff818ca4 | ||
|
|
02ef7c8d90 | ||
|
|
e354fcb3bf | ||
|
|
4e3a7a6de2 | ||
|
|
0263f8fe09 | ||
|
|
a6ff022c0e | ||
|
|
bd2ffe5078 | ||
|
|
96b7f9d4af | ||
|
|
cfbc47488a | ||
|
|
05812c6be1 | ||
|
|
18452cc53c | ||
|
|
e0236ea52d | ||
|
|
6f4f6bfd03 | ||
|
|
f8520c27a2 | ||
|
|
61e7c00b62 | ||
|
|
ed41d8e45f | ||
|
|
80f3331187 | ||
|
|
9a7ff9fe27 | ||
|
|
12b6ed3dbb | ||
|
|
48acb9ca38 | ||
|
|
deae36d8c8 | ||
|
|
947ce59eb9 | ||
|
|
9be861dbc0 | ||
|
|
c58b89d74a | ||
|
|
8fb7067531 | ||
|
|
d3b0d6ca57 | ||
|
|
0db6c6ab5f | ||
|
|
d0fb4ee2f6 | ||
|
|
366ca05200 | ||
|
|
ed7a2bd239 | ||
|
|
6ebf273390 | ||
|
|
9d488b0e61 | ||
|
|
082dd3af41 | ||
|
|
589606eef0 | ||
|
|
1c1682a192 | ||
|
|
1ad946d013 | ||
|
|
5e4520e10c | ||
|
|
1ac4f05d91 | ||
|
|
9e3d6c6e23 | ||
|
|
3306fadc47 | ||
|
|
9e2b9a9fcd | ||
|
|
780e1f9b92 | ||
|
|
547953eb31 | ||
|
|
59652d2d61 | ||
|
|
daf02e53f3 | ||
|
|
4a8fea8456 | ||
|
|
a442322729 | ||
|
|
34f730a196 | ||
|
|
cb7dafba4d | ||
|
|
9c0d31997a | ||
|
|
2ac71fe4ad | ||
|
|
5b2b7b0294 | ||
|
|
612b5a81d2 | ||
|
|
0dda102a05 | ||
|
|
113003f485 | ||
|
|
6a1b83b365 | ||
|
|
7ff5232554 | ||
|
|
a761bdcaaa | ||
|
|
c09106472f | ||
|
|
e5a176f5ea | ||
|
|
5987ccdbc9 | ||
|
|
423f5ba41d | ||
|
|
d00c953c88 | ||
|
|
02ea6c4359 | ||
|
|
2aeeb701c8 | ||
|
|
1395d506cf | ||
|
|
e22414d908 | ||
|
|
17c4f87fa5 | ||
|
|
3ba75fecba | ||
|
|
d87c46ff03 | ||
|
|
6ad92b9efd | ||
|
|
6d477aa6ec | ||
|
|
0dedeb0180 | ||
|
|
cc9bc3d6da | ||
|
|
03a8e6a3ec | ||
|
|
486bbba4ca | ||
|
|
55d7d52e35 | ||
|
|
8c6f0adc66 | ||
|
|
13cb08f64a | ||
|
|
83a71b089c | ||
|
|
6c9812ee7f | ||
|
|
22988b4bbc | ||
|
|
ec8da2b72b | ||
|
|
a969179cbf | ||
|
|
50265b3b37 | ||
|
|
ae74f21242 | ||
|
|
25428c7e9d | ||
|
|
97c0f81a2e | ||
|
|
b8f7f751b0 | ||
|
|
c33dd8f856 | ||
|
|
fa15742edb | ||
|
|
aa52d2e8f8 | ||
|
|
3fb700f163 | ||
|
|
2f90d95c4c | ||
|
|
b3a94c3524 | ||
|
|
15f4ba54df | ||
|
|
505e6e2a0d | ||
|
|
c0ed491f7a | ||
|
|
166b790a3e | ||
|
|
d2f3aa1c10 | ||
|
|
4e8f12a392 | ||
|
|
cc04a82bfe | ||
|
|
3a8af7de29 | ||
|
|
85fb2ff44d | ||
|
|
f89a8c3eb5 | ||
|
|
de26a00c49 | ||
|
|
f21dd94f2c | ||
|
|
4a56d2d087 | ||
|
|
e4316e6665 | ||
|
|
ad81e3f9e9 | ||
|
|
d10e4ba22a | ||
|
|
becaaaa2fd | ||
|
|
082f1058ce | ||
|
|
0b495b1fa5 | ||
|
|
3161961faa | ||
|
|
9303c48e01 | ||
|
|
b214ba0054 | ||
|
|
2ea68e88b0 | ||
|
|
93869315c1 | ||
|
|
d7bf80ea79 | ||
|
|
3ff2c4fb82 | ||
|
|
b70860c903 | ||
|
|
dc49a00f5f | ||
|
|
50e30dbb6e | ||
|
|
95436347e5 | ||
|
|
57d1021d51 | ||
|
|
10450dea8b | ||
|
|
37fd42448f | ||
|
|
04607318ac | ||
|
|
ed0223fbb8 | ||
|
|
823ad4777f | ||
|
|
34a8ff3ce3 | ||
|
|
637ec00d8f | ||
|
|
1a19d25fdb | ||
|
|
cd5ba2b000 | ||
|
|
f644430a78 | ||
|
|
a3d9d98184 | ||
|
|
325197175b | ||
|
|
60c35f7c12 | ||
|
|
f8d358a4f3 | ||
|
|
7a1652972d | ||
|
|
288ff23f2b | ||
|
|
7f64dee54b | ||
|
|
9771eced99 | ||
|
|
1a4e2c5d35 | ||
|
|
54ef583c41 | ||
|
|
9fa5b6bf9e | ||
|
|
8ceb4e8fb3 | ||
|
|
7803bfe429 | ||
|
|
63c5dbb93e | ||
|
|
c0a198faaa | ||
|
|
448d5c4cfc | ||
|
|
bebee2f32c | ||
|
|
a39f09773b | ||
|
|
4dc9dfa586 | ||
|
|
0893a2ff20 | ||
|
|
cc8ce3f679 | ||
|
|
ae5c1db83d | ||
|
|
acaa5bae55 | ||
|
|
f49f606b0e | ||
|
|
fad4dea1c5 | ||
|
|
f177bb2f31 | ||
|
|
47d18cafa3 | ||
|
|
dcfe7d3f2d | ||
|
|
60abc83ff1 | ||
|
|
139d3e6651 | ||
|
|
7f7ee97106 | ||
|
|
9f3f41f308 | ||
|
|
eda85cb580 | ||
|
|
16f71ab219 | ||
|
|
28aefdddda | ||
|
|
26cc746619 | ||
|
|
c872e5afe4 | ||
|
|
5a63b02782 | ||
|
|
0b494a13a5 | ||
|
|
700543c573 | ||
|
|
e867000ff7 | ||
|
|
b2b3dac35b | ||
|
|
8d11ad43da | ||
|
|
614715f398 | ||
|
|
021b332838 | ||
|
|
43f54b30c6 | ||
|
|
e1b028652e | ||
|
|
944d1173d5 | ||
|
|
3e18c5286a | ||
|
|
d2ff0bcc0b | ||
|
|
26c3a2c943 | ||
|
|
5d52382c7a | ||
|
|
fdd1d98658 | ||
|
|
009eb749fe | ||
|
|
dfe5bba285 | ||
|
|
70e4fedc4d | ||
|
|
ca3d3b44ce | ||
|
|
13c76d395e | ||
|
|
ca67e9fb61 | ||
|
|
740e239d2e | ||
|
|
bc6abee577 | ||
|
|
5d657abd46 | ||
|
|
a3fb5a30ee | ||
|
|
13543e314e | ||
|
|
4538123526 | ||
|
|
3bac68dffb | ||
|
|
02e6198f17 | ||
|
|
e0219c020a | ||
|
|
92ed4fcc81 | ||
|
|
7beb7120a9 | ||
|
|
2daa299b72 | ||
|
|
3e7eb46f33 | ||
|
|
b840e5244b | ||
|
|
d40f211995 | ||
|
|
f4a11c9c6e | ||
|
|
e68179c752 | ||
|
|
ba46e995cd | ||
|
|
9954e25b65 | ||
|
|
9633956b99 | ||
|
|
1acaa5d542 | ||
|
|
5323a1b00f | ||
|
|
44c040707c | ||
|
|
ba400a9c4f | ||
|
|
6d15cabfe5 | ||
|
|
c2e558c9b1 | ||
|
|
c6b9f2276b | ||
|
|
82fb6fe7e6 | ||
|
|
caf1ee3e2a | ||
|
|
55b8620186 | ||
|
|
998eb8c4bb | ||
|
|
614c7ace44 | ||
|
|
92ab56bf5e | ||
|
|
dd39d18e2a | ||
|
|
99c79a8fa8 | ||
|
|
047fd62ceb | ||
|
|
68218e43dc | ||
|
|
365a5effdf | ||
|
|
69cd876895 | ||
|
|
ab2f646496 | ||
|
|
4bb1b41505 | ||
|
|
aa91623d35 | ||
|
|
df1e5926df | ||
|
|
6ef3390080 | ||
|
|
8d3f4f5e1d | ||
|
|
92bfa7bd75 | ||
|
|
429a0eb4e0 | ||
|
|
478087fe9e | ||
|
|
4dc7ad02d5 | ||
|
|
b5dbd0cc7a | ||
|
|
4365892927 | ||
|
|
4ad25188b2 | ||
|
|
d069ca09a7 | ||
|
|
97391a3f33 | ||
|
|
263dbab22a | ||
|
|
3d845f67ee | ||
|
|
0cbeba29d1 | ||
|
|
e81cf80ac9 | ||
|
|
c3c75c216b | ||
|
|
0dcc468ae1 | ||
|
|
23fe3a946b | ||
|
|
f8af48cf47 | ||
|
|
2786485de2 | ||
|
|
ad3313fda4 | ||
|
|
c0c0106abe | ||
|
|
34f5f83978 | ||
|
|
1f5613b504 | ||
|
|
c0d3c352b8 | ||
|
|
3c178a5d1d | ||
|
|
f33e2315fc | ||
|
|
0dbf992494 | ||
|
|
3d0b16600c | ||
|
|
cda2aad98d | ||
|
|
1d8ed48666 | ||
|
|
6cafd65690 | ||
|
|
73ac962289 | ||
|
|
cc360837b1 | ||
|
|
1ec4930dfa | ||
|
|
e9f2b71574 | ||
|
|
53a6f04f18 | ||
|
|
0aa48a8dd4 | ||
|
|
685b2ea93a | ||
|
|
f9b5a37555 | ||
|
|
0c081cf9aa | ||
|
|
9ec8ac8bba | ||
|
|
f4e81a183e | ||
|
|
c73736a05f | ||
|
|
4b63adb0d5 | ||
|
|
cd8b3936fb | ||
|
|
fdc019e43c | ||
|
|
944edef596 | ||
|
|
7df1d55162 | ||
|
|
b7a11d42de | ||
|
|
372e4bc1c0 | ||
|
|
17fc37975c | ||
|
|
aaae0a314c | ||
|
|
2656d362d6 | ||
|
|
24dbe4f197 | ||
|
|
d198ad5a59 | ||
|
|
b4dbf249ee | ||
|
|
6739ec21c8 | ||
|
|
c9d3f3de4a | ||
|
|
027ce6ccae | ||
|
|
1b0962cce1 | ||
|
|
325c748acc | ||
|
|
ff80235ee8 | ||
|
|
e304b1dd26 | ||
|
|
0ff66f8df8 | ||
|
|
f2c4b8a1b3 | ||
|
|
e554bcc27e | ||
|
|
3a6a5a6925 | ||
|
|
1592c84ac0 | ||
|
|
f8790a3a89 | ||
|
|
326d35eee7 | ||
|
|
daf0f089b8 | ||
|
|
8a461e7a3d | ||
|
|
24681f8955 | ||
|
|
fffc044998 | ||
|
|
3ced479ac9 | ||
|
|
41fc41984a | ||
|
|
bcc2c7b9ca | ||
|
|
969dca5f7e | ||
|
|
9510347d3a | ||
|
|
d59a326920 | ||
|
|
2bcaf3c6ad | ||
|
|
3988ea9ba5 | ||
|
|
ed673fdba8 | ||
|
|
ad417c743e | ||
|
|
cab81d5214 | ||
|
|
78178ecc2f | ||
|
|
84ffe1e552 | ||
|
|
f6b440adef | ||
|
|
9b309c31bd | ||
|
|
8e0303f065 | ||
|
|
a424c6a52a | ||
|
|
c4acb26758 | ||
|
|
a9e5f1c6f8 | ||
|
|
b9e2d6caca | ||
|
|
a76564fce7 | ||
|
|
8056bf8959 | ||
|
|
c673f5fb7a | ||
|
|
a828c3e075 | ||
|
|
a745972cd4 | ||
|
|
29d3010c80 | ||
|
|
4ded93cc0a | ||
|
|
7c79b0ecb7 | ||
|
|
274fd9de77 | ||
|
|
5580ada03f | ||
|
|
2a56595cf9 | ||
|
|
04896f5d88 | ||
|
|
bdd9a80db1 | ||
|
|
336f7d482e | ||
|
|
721e73101c | ||
|
|
8c9468da84 | ||
|
|
143050176b | ||
|
|
3017f8c656 | ||
|
|
987ed0fcba | ||
|
|
6fe336e66c | ||
|
|
62f2fe4f52 | ||
|
|
47e427a892 | ||
|
|
74229478ae | ||
|
|
6be84f8ab4 | ||
|
|
7213395118 | ||
|
|
35db1f4eb8 | ||
|
|
b6df50546e | ||
|
|
f745a54669 | ||
|
|
652fbced3c | ||
|
|
c77edf37e1 | ||
|
|
a70a7c4c95 | ||
|
|
f38e5a41cd | ||
|
|
b96cb41876 | ||
|
|
f9048d1209 | ||
|
|
07710f685f | ||
|
|
57fbdf8795 | ||
|
|
015dcbdc41 | ||
|
|
c14d7bdd46 | ||
|
|
25214f2d87 | ||
|
|
7f94c26bee | ||
|
|
c1e25dedb9 | ||
|
|
55642e826e | ||
|
|
31d929912a | ||
|
|
40622d45b7 | ||
|
|
76b6f32222 | ||
|
|
d0ee5325f4 | ||
|
|
93a4d5ba88 | ||
|
|
8bcd0a3521 | ||
|
|
248dcb8257 | ||
|
|
fdc2d5a490 | ||
|
|
c34f99dd6e | ||
|
|
43622b50dc | ||
|
|
759933e24b | ||
|
|
59a02af2b7 | ||
|
|
d65a88305c | ||
|
|
2a116612be | ||
|
|
6df5802f97 | ||
|
|
b3c10190cb | ||
|
|
3513669412 | ||
|
|
b45459d645 | ||
|
|
60de50da64 | ||
|
|
98a45e383b | ||
|
|
b46f5c9ec2 | ||
|
|
c4e98c8b4c | ||
|
|
4fc04648cf | ||
|
|
d5690fc579 | ||
|
|
068922f05f | ||
|
|
705822fe7a | ||
|
|
f0cf39e297 | ||
|
|
8c077a94c6 | ||
|
|
6bd66b8a2d | ||
|
|
a08d376023 | ||
|
|
94aa2f11c4 | ||
|
|
173a172cf8 | ||
|
|
d59563cf1b | ||
|
|
386fd936b4 | ||
|
|
7450afeecf | ||
|
|
70b1a93108 | ||
|
|
0d34d0b25b | ||
|
|
d2a99ad7f2 | ||
|
|
0aadf987ac | ||
|
|
dfa3ffecd0 | ||
|
|
f36c9ab7d6 | ||
|
|
dfdfeb2641 | ||
|
|
1db3952ea4 | ||
|
|
abaf5a96fa | ||
|
|
165ac87685 | ||
|
|
cc03185ea0 | ||
|
|
eaf405e250 | ||
|
|
c2c72dc4be | ||
|
|
b98b21c1c6 | ||
|
|
570c7e1a1a | ||
|
|
190db75c19 | ||
|
|
9bc56a5abd | ||
|
|
a241149b6b | ||
|
|
b3afbe107d | ||
|
|
e2b08ba160 | ||
|
|
2cdc8d0055 | ||
|
|
195f14ccb8 | ||
|
|
7ae80226d8 | ||
|
|
2d1a760df3 | ||
|
|
b99c405202 | ||
|
|
58ad745373 | ||
|
|
9ccd616132 | ||
|
|
8c033f53fa | ||
|
|
9427b36633 | ||
|
|
5657af7238 | ||
|
|
89e80bf124 | ||
|
|
0fd0d18154 | ||
|
|
ce63c36fb0 | ||
|
|
71f78a83bb | ||
|
|
8f417c53fb | ||
|
|
7c6d2ad423 | ||
|
|
c18ae6477d | ||
|
|
d04f2166d9 | ||
|
|
a0e7bf790e | ||
|
|
df0787d8b0 | ||
|
|
bd21c98179 | ||
|
|
46bd6d4564 | ||
|
|
c3ca3d7732 | ||
|
|
56950b4b70 | ||
|
|
d0be08ddd3 | ||
|
|
106765e384 | ||
|
|
d784c61076 | ||
|
|
f6e3017b50 | ||
|
|
4fd282dc6c | ||
|
|
a0576dba1f | ||
|
|
e04cc1080a | ||
|
|
4ebb3e7082 | ||
|
|
aa7323ba43 | ||
|
|
d2167dd290 | ||
|
|
891195efa6 | ||
|
|
295b3e4cfb | ||
|
|
03e2dda6c1 | ||
|
|
b89f91f08a | ||
|
|
4fd2f63c07 | ||
|
|
5c21fea5d7 | ||
|
|
ca249bcced | ||
|
|
696a2bbd3c | ||
|
|
fd54796710 | ||
|
|
521f5e0d08 | ||
|
|
dd662164d0 | ||
|
|
9e562ac882 | ||
|
|
a093eb3231 | ||
|
|
7999894bda | ||
|
|
56ede16833 | ||
|
|
705907e816 | ||
|
|
7d66adbe3c | ||
|
|
f726ac7061 | ||
|
|
df7b11eadb | ||
|
|
de150b359c | ||
|
|
1e55f86f51 | ||
|
|
ea4cda6a91 | ||
|
|
18a859c68f | ||
|
|
cc22327279 | ||
|
|
e07749be6a | ||
|
|
986b3a79f1 | ||
|
|
c450965cb6 | ||
|
|
7459813646 | ||
|
|
913ed54668 | ||
|
|
84c8e0d08d | ||
|
|
d498bf2b53 | ||
|
|
8a3948777f | ||
|
|
9fc4d8f953 | ||
|
|
f32f5347a7 | ||
|
|
6e24f45d4a | ||
|
|
27f5ba9337 | ||
|
|
8e6f35bfad | ||
|
|
565e8f0fb7 | ||
|
|
8fe5cd3c95 | ||
|
|
c1385894cc | ||
|
|
91d9da4046 | ||
|
|
ae725b0974 | ||
|
|
fd5a793de0 | ||
|
|
54723ea522 | ||
|
|
c8ea6fd2aa | ||
|
|
382ccdb499 | ||
|
|
2d920396b2 | ||
|
|
b7fc061f04 | ||
|
|
876ed221c3 | ||
|
|
3bd073a38b | ||
|
|
34106c937e | ||
|
|
d1466ad2f2 | ||
|
|
574fd820a6 | ||
|
|
4006dd38f8 | ||
|
|
b5062624fa | ||
|
|
5aa71a3ede | ||
|
|
d57300dd75 | ||
|
|
464d22d69a | ||
|
|
7d485813e5 | ||
|
|
644c8ad389 | ||
|
|
870019d2df | ||
|
|
27fbe01876 | ||
|
|
b2009f6bc0 | ||
|
|
9db2690710 | ||
|
|
0e1a34f067 | ||
|
|
b07bf3ad12 | ||
|
|
1357a696be | ||
|
|
e97cf8fa6b | ||
|
|
e3f781cffb | ||
|
|
ca170f2461 | ||
|
|
766d3eaa98 | ||
|
|
a8e38a998f | ||
|
|
12711be049 | ||
|
|
4276896002 | ||
|
|
39ad99224e | ||
|
|
62bda28a28 | ||
|
|
acf1be454f | ||
|
|
bb3fc79e09 | ||
|
|
ab1a75935f | ||
|
|
05b1e96046 | ||
|
|
2876f92f4d | ||
|
|
667e50c9fb | ||
|
|
4ee00d30ae | ||
|
|
47cd902369 | ||
|
|
ee3aa16fcf | ||
|
|
a40e6484a1 | ||
|
|
33f0dfd3b2 | ||
|
|
4af874e1d1 | ||
|
|
df8f13e359 | ||
|
|
0f0f60e456 | ||
|
|
97a369048e | ||
|
|
dde1183174 | ||
|
|
513fc949d7 | ||
|
|
9e05aa4a50 | ||
|
|
41969bf2c1 | ||
|
|
0e202a6640 | ||
|
|
4d2d223bdb | ||
|
|
ae8d40786e | ||
|
|
1cff9d987e | ||
|
|
380eae6669 | ||
|
|
8e0414a4ec | ||
|
|
d6acf55ab2 | ||
|
|
6169f6083f | ||
|
|
c0247ff2bd | ||
|
|
2efe90fe48 | ||
|
|
d84f39be86 | ||
|
|
c1b7d37663 | ||
|
|
5b3b362012 | ||
|
|
490465cbbf | ||
|
|
e880d0ce86 | ||
|
|
0b092a588d | ||
|
|
593fd4c41d | ||
|
|
14bee5d3bd | ||
|
|
4303ee1908 | ||
|
|
df963e3537 | ||
|
|
6e56808a0d | ||
|
|
57003ab4b8 | ||
|
|
a95ecf6133 | ||
|
|
47793ce1cb | ||
|
|
b3893f7164 | ||
|
|
0cd39afadc | ||
|
|
37dee0e78e | ||
|
|
7e6523b59a | ||
|
|
1722d88f1a | ||
|
|
44bda3a315 | ||
|
|
719b21a4fb | ||
|
|
68ed7cafae | ||
|
|
6bb77d6d7c | ||
|
|
150d95c157 | ||
|
|
3d176d4e4f | ||
|
|
c73e3637a3 | ||
|
|
31d9e9fd1b | ||
|
|
20f6dea345 | ||
|
|
0763f3d1c6 | ||
|
|
f4554374bd | ||
|
|
b0211424a7 | ||
|
|
ebf7540554 | ||
|
|
275ad40346 | ||
|
|
28d8bec935 | ||
|
|
7cacc9b503 | ||
|
|
0f7505baba | ||
|
|
847d33abc6 | ||
|
|
182f884374 | ||
|
|
97b65465e2 | ||
|
|
20daa1ce13 | ||
|
|
091895df38 | ||
|
|
6141ca7936 | ||
|
|
6f98aee998 | ||
|
|
7bc302c665 | ||
|
|
50569e1854 | ||
|
|
0cbca605ec | ||
|
|
ab748aab99 | ||
|
|
2559c09b23 | ||
|
|
12cbb9f07e | ||
|
|
99b7ec661c | ||
|
|
b4e146776f | ||
|
|
118587dcf3 | ||
|
|
6e1aae3f61 | ||
|
|
9006c377fd | ||
|
|
886e53bb8f | ||
|
|
e1dced8007 | ||
|
|
cbfc7e7d82 | ||
|
|
9ef11db895 | ||
|
|
044b0dcde6 | ||
|
|
dcfa144ce3 | ||
|
|
58d3fc8fd5 | ||
|
|
cfc497556e | ||
|
|
007e4b25eb | ||
|
|
c6aad25581 | ||
|
|
5262e59fdd | ||
|
|
57ce6f9bbf | ||
|
|
e3830a5c3f | ||
|
|
5552d3c1b9 | ||
|
|
a98f8aa1f6 | ||
|
|
1e03eac9a5 | ||
|
|
8aef85804e | ||
|
|
2b0ca5d314 | ||
|
|
a7041b4efb | ||
|
|
6f33bc23d3 | ||
|
|
6ac5cea2e4 | ||
|
|
70a7784d68 | ||
|
|
8dac28e1a2 | ||
|
|
383e40636b | ||
|
|
f94fc34d4f | ||
|
|
0efc0bbe3a | ||
|
|
453e73f71a | ||
|
|
9941ff61f7 | ||
|
|
aebed32f76 | ||
|
|
d5d47ffed1 | ||
|
|
a473e5e223 | ||
|
|
d5a43e422d | ||
|
|
e9a63b40c5 | ||
|
|
ca12655e5f | ||
|
|
6ce08726b4 | ||
|
|
3d168b4688 | ||
|
|
5a2d2fd0e5 | ||
|
|
44a623543f | ||
|
|
18d0ba4acb | ||
|
|
1762198642 | ||
|
|
196dffa8d8 | ||
|
|
4b8a295fbe | ||
|
|
ec0d92bfac | ||
|
|
479452ad7b | ||
|
|
f5f3a90275 | ||
|
|
f06b6edef5 | ||
|
|
0063c2fc3f | ||
|
|
7ae53719c4 | ||
|
|
9c9adde9c8 | ||
|
|
568caed3c7 | ||
|
|
3f08051e37 | ||
|
|
a22ae5fcf4 | ||
|
|
10ec90435d | ||
|
|
2f2079e9c1 | ||
|
|
2fa3508c11 | ||
|
|
55e55b7bb7 | ||
|
|
9a5a5091a1 | ||
|
|
a9b60a762f | ||
|
|
f181863d50 | ||
|
|
f53d9ed6c3 | ||
|
|
4df3766231 | ||
|
|
128e2f6254 | ||
|
|
f83b60782a | ||
|
|
6fa4dff23b | ||
|
|
33de51b2d5 | ||
|
|
8a10d30f2a | ||
|
|
8a0b3d3791 | ||
|
|
86f084e5a9 | ||
|
|
785bde153e | ||
|
|
96d08cd418 | ||
|
|
ae4a90bda5 | ||
|
|
859b064d39 | ||
|
|
dd5bb8593c | ||
|
|
53940e64ab | ||
|
|
dceb5fca41 | ||
|
|
5a69d1e241 | ||
|
|
e02cb38974 | ||
|
|
662121d90e | ||
|
|
2dbcd47a56 | ||
|
|
7b3102a6b3 | ||
|
|
3648b3eb60 | ||
|
|
72815f842e | ||
|
|
72b481f9c4 | ||
|
|
6982b57289 | ||
|
|
f89a1cd3f9 | ||
|
|
b3e1fe1320 | ||
|
|
9f2b2e50d7 | ||
|
|
e4c7cec7e3 | ||
|
|
39c17b6493 | ||
|
|
312f1c3b9b | ||
|
|
27d119c068 | ||
|
|
2a14566679 | ||
|
|
306d3ec7b5 | ||
|
|
7e304daee0 | ||
|
|
304c67534b | ||
|
|
de7e9d86d3 | ||
|
|
e727c54798 | ||
|
|
32e3f829fa | ||
|
|
b0d4b56cee | ||
|
|
a0f7114a39 | ||
|
|
905315b164 | ||
|
|
ec111defbb | ||
|
|
ba55b328ea | ||
|
|
7ab9d1a980 | ||
|
|
3c464cace5 | ||
|
|
9933a0e83e | ||
|
|
9cceafcb3e | ||
|
|
2ed6ccf89c | ||
|
|
8ab32816e2 | ||
|
|
355f6158be | ||
|
|
7da5692865 | ||
|
|
a45cd5904f | ||
|
|
c72c2dbaa4 | ||
|
|
4c84c6cdfc | ||
|
|
ec5d570ac6 | ||
|
|
17cb093b32 | ||
|
|
e28085fbba | ||
|
|
62f0bac381 | ||
|
|
bfb0874c47 | ||
|
|
7f40c317d0 | ||
|
|
ea55eb13e5 | ||
|
|
7c1f70c51e | ||
|
|
6c144b5a28 | ||
|
|
7ca5991041 | ||
|
|
5225b62a0a | ||
|
|
bb906e61ab | ||
|
|
e1a51ba69c | ||
|
|
a25d35d6df | ||
|
|
d1154df74d | ||
|
|
81f8fe3019 | ||
|
|
43cbbd51c1 | ||
|
|
83aa65981b | ||
|
|
a056631cef | ||
|
|
89b6e84fea | ||
|
|
d832117ae4 | ||
|
|
8f037b4b46 | ||
|
|
a7f764546f | ||
|
|
f8002151be | ||
|
|
6c01db395d | ||
|
|
bac6c2d332 | ||
|
|
59fea00a31 | ||
|
|
66c9091b88 | ||
|
|
f0d97b52d9 | ||
|
|
a46484755b | ||
|
|
728286d16c | ||
|
|
04a78ad8bd | ||
|
|
d55ce60cc5 | ||
|
|
d2c59d2e1c | ||
|
|
19946fc551 | ||
|
|
d6be75e39d | ||
|
|
ee6654474d | ||
|
|
453e9b0f3e | ||
|
|
e5e6ad8aac | ||
|
|
c365ffedf3 | ||
|
|
dc2a72c756 | ||
|
|
83659ff62d | ||
|
|
e1d95f8632 | ||
|
|
cbcab5b981 | ||
|
|
3ec93d11ea | ||
|
|
8c3a4b4fc7 | ||
|
|
8948d22095 | ||
|
|
5e0d52b9e7 | ||
|
|
d4cdfb8fa5 | ||
|
|
a9669e430f | ||
|
|
be633f78a7 | ||
|
|
ea07ea45b8 | ||
|
|
809fca73a0 | ||
|
|
2084913a4d | ||
|
|
673126ae8a | ||
|
|
b35eab77be | ||
|
|
c04a5da865 | ||
|
|
a24a7dbde7 | ||
|
|
be8d302b82 | ||
|
|
ec9197cb85 | ||
|
|
0914ede3a3 | ||
|
|
f105ed3f9d | ||
|
|
84aa997c62 | ||
|
|
a352ebff25 | ||
|
|
9eb5646aec | ||
|
|
e6db60e76c | ||
|
|
070d10a1d0 | ||
|
|
0a6ac6613c | ||
|
|
7eba9cf48e | ||
|
|
3a37aad25f | ||
|
|
91595ef6d5 | ||
|
|
4b8ad326fa | ||
|
|
443a6df070 | ||
|
|
29f6b2b743 | ||
|
|
b726400f42 | ||
|
|
5e35e39012 | ||
|
|
61c533a1a6 | ||
|
|
5e012bd66e | ||
|
|
a80cb495d2 | ||
|
|
aca3490331 | ||
|
|
69d92ee184 | ||
|
|
f0bb3a8a45 | ||
|
|
e133cf5ffb | ||
|
|
b7bab3cf21 | ||
|
|
9558456bcf | ||
|
|
8749d189df | ||
|
|
4d9fb41d73 | ||
|
|
5747b7add6 | ||
|
|
dac5b105ce | ||
|
|
58955f792a | ||
|
|
8292c47f7c | ||
|
|
b947db2eb1 | ||
|
|
59c0f5cc94 | ||
|
|
081726d764 | ||
|
|
55a5fb491a | ||
|
|
48c3d655b4 | ||
|
|
ad985f94e5 | ||
|
|
7cf5edc74a | ||
|
|
b1c435277c | ||
|
|
2d829178f8 | ||
|
|
b89a1b309c | ||
|
|
e965d58de0 | ||
|
|
8f9c199360 | ||
|
|
816a2745bc | ||
|
|
34e4bad77f | ||
|
|
e4f3df3bb9 | ||
|
|
f6eab84d40 | ||
|
|
84f739c72f | ||
|
|
ec937a5af1 | ||
|
|
30ed8dde11 | ||
|
|
06fdf66748 | ||
|
|
6b3f76e4ee | ||
|
|
2a3cea63cc | ||
|
|
6b3c073ddd | ||
|
|
c651112cc0 | ||
|
|
f7f3b0d373 | ||
|
|
c9f14318ea | ||
|
|
e55938b653 | ||
|
|
81ccee1152 | ||
|
|
6a715da1ac | ||
|
|
a96ff94c6f | ||
|
|
f3935b1df4 | ||
|
|
62639acc5a | ||
|
|
42bcc0e047 | ||
|
|
9ebfa94fec | ||
|
|
f96bb39204 | ||
|
|
f1350c83b7 | ||
|
|
55020137a9 | ||
|
|
4a2de54c3e | ||
|
|
4d66c02190 | ||
|
|
0a8dfe7d51 | ||
|
|
38ef44e1d0 | ||
|
|
2467800247 | ||
|
|
b2477f2ce3 | ||
|
|
167c27c1ee | ||
|
|
b88794b54b | ||
|
|
1a75a624cb | ||
|
|
ef850e37f7 | ||
|
|
501b0daa09 | ||
|
|
7baa7af929 | ||
|
|
508f5cc6e5 | ||
|
|
98b203a3ca | ||
|
|
d21b7e754f | ||
|
|
4606ddf182 | ||
|
|
1a331dc3ba | ||
|
|
4400af04c7 | ||
|
|
adaac297f9 | ||
|
|
e67776adf4 | ||
|
|
aae22db190 | ||
|
|
64c9d1cd40 | ||
|
|
d260d97136 | ||
|
|
6bffd8e859 | ||
|
|
2e892ff083 | ||
|
|
4ad603eeb1 | ||
|
|
a8647cbcae | ||
|
|
4521f07acf | ||
|
|
dd46434895 | ||
|
|
0a4f83f507 | ||
|
|
d1e6ea379e | ||
|
|
3a46916ad9 | ||
|
|
184d3fe7d6 | ||
|
|
1d652e0ce3 | ||
|
|
f1c6ff3f2a | ||
|
|
d1bab197c1 | ||
|
|
927c4e6ed1 | ||
|
|
9fbe7e8d13 | ||
|
|
e0fa23822c | ||
|
|
c823ef6eae | ||
|
|
7f0315dae4 | ||
|
|
2916583d0b | ||
|
|
638755bef3 | ||
|
|
2788ff3bcf | ||
|
|
339ed3d44e | ||
|
|
b4c6a3f18e | ||
|
|
c9d0a530c2 | ||
|
|
02366067c0 | ||
|
|
f490c53919 | ||
|
|
f47b77347d | ||
|
|
8ce2a244e9 | ||
|
|
c3f7827683 | ||
|
|
c1c851c238 | ||
|
|
08257f47cb | ||
|
|
65d469f71a | ||
|
|
bfb914132e | ||
|
|
6f8abf33c8 | ||
|
|
4bb1e24222 | ||
|
|
c61f4a6918 | ||
|
|
3db4827354 | ||
|
|
e3219a2636 | ||
|
|
ddc953f4fa | ||
|
|
7d45dce7ce | ||
|
|
91a505ea02 | ||
|
|
9d7f93f304 | ||
|
|
6d58b1efb5 | ||
|
|
e56415aa51 | ||
|
|
8984726fd7 | ||
|
|
66d9300b61 | ||
|
|
ca894d4427 | ||
|
|
051af6132e | ||
|
|
a4724df69e | ||
|
|
9675d03568 | ||
|
|
f008f948d2 | ||
|
|
853727541f | ||
|
|
fa15ff6ba4 | ||
|
|
5d6fb0c705 | ||
|
|
7e2cc69a23 | ||
|
|
e2149ea94c | ||
|
|
9198ce187b | ||
|
|
039e2252de | ||
|
|
a5d749ec05 | ||
|
|
280660314f | ||
|
|
58666c0df5 | ||
|
|
28ee711938 | ||
|
|
7441b64de0 | ||
|
|
cbfb83e4db | ||
|
|
2548f30a2a | ||
|
|
0c6895f7d7 | ||
|
|
95ec5e6bb0 | ||
|
|
b7b266defc | ||
|
|
7bd76a19b5 | ||
|
|
f869ed5121 | ||
|
|
870d78d5a3 | ||
|
|
089c229213 | ||
|
|
13f8ab5caf | ||
|
|
be70870b72 | ||
|
|
cabc652aa2 | ||
|
|
c61c3c6ce7 | ||
|
|
84169179bc | ||
|
|
09bd6d5335 | ||
|
|
758ce9805e | ||
|
|
20058edd71 | ||
|
|
ef5c3f73a5 | ||
|
|
2400c6def8 | ||
|
|
56a5f29f26 | ||
|
|
465d7ed673 | ||
|
|
cf910c678b | ||
|
|
e6b2cdfcdf | ||
|
|
b5552c1408 | ||
|
|
a5da1eee65 | ||
|
|
67e1e6ab05 | ||
|
|
57c000908f | ||
|
|
7e30c50dfe | ||
|
|
004de4e48c | ||
|
|
b0fea1e7c1 | ||
|
|
f8d990a25f | ||
|
|
e9ad4e38c2 | ||
|
|
db629bf29c | ||
|
|
fb8d557099 | ||
|
|
d75f64904f | ||
|
|
b05e4f609c | ||
|
|
56a0fd63ce | ||
|
|
3db4ff2a82 | ||
|
|
c1017499e4 | ||
|
|
603d48f739 | ||
|
|
44ddb2d78e | ||
|
|
5412348a65 | ||
|
|
5effa242ae | ||
|
|
086f5ebb06 | ||
|
|
fdc21f26fc | ||
|
|
daf1e2769f | ||
|
|
771c6f7559 | ||
|
|
612aa09071 | ||
|
|
7135af995d | ||
|
|
5e2d9c4fb0 | ||
|
|
77d2d099e2 | ||
|
|
eb802135a9 | ||
|
|
df7888e3a6 | ||
|
|
9d876d1146 | ||
|
|
9e124f0572 | ||
|
|
787907f42a | ||
|
|
00302d2064 | ||
|
|
85c5168cb6 | ||
|
|
a2b638dc55 | ||
|
|
bcb6d30023 | ||
|
|
0423c50c51 | ||
|
|
fc0074dfd2 | ||
|
|
1db1bdf675 | ||
|
|
beb2c7f577 | ||
|
|
b58006503c | ||
|
|
25bd3d441e | ||
|
|
677e1863e5 | ||
|
|
50f73dde75 | ||
|
|
eb6ac32798 | ||
|
|
23d1eb0e28 | ||
|
|
5047c3c6b8 | ||
|
|
7183e2614e | ||
|
|
1ad64e9fc9 | ||
|
|
ee99590a63 | ||
|
|
aabce7c951 | ||
|
|
bde5060008 | ||
|
|
0736782a44 | ||
|
|
7dc4002fb4 | ||
|
|
60700a56e9 | ||
|
|
60964c9869 | ||
|
|
ad3d8c87ec | ||
|
|
6f2f89c365 | ||
|
|
1e597f9cd7 | ||
|
|
79348f975f | ||
|
|
cb2f641378 | ||
|
|
0dfcc52728 | ||
|
|
288eedf6a1 | ||
|
|
3bff3555a0 | ||
|
|
4f5db79e27 | ||
|
|
4c0b0b102a | ||
|
|
5cdbba8da0 | ||
|
|
d74407f36b | ||
|
|
40a352c3c0 | ||
|
|
9e2d8d02b9 | ||
|
|
9ee2f46a99 | ||
|
|
a0eac266f7 | ||
|
|
aa9888d2df | ||
|
|
b77bf0745e | ||
|
|
e10bc516fe | ||
|
|
f6764b3616 | ||
|
|
50fef1cf69 | ||
|
|
9fcf2658d0 | ||
|
|
81a1b12005 | ||
|
|
366ef575f1 | ||
|
|
9ded40ba9c | ||
|
|
8247357184 | ||
|
|
930be49775 | ||
|
|
24dbff999e | ||
|
|
6552a93989 | ||
|
|
87381e2e4d | ||
|
|
36c8b97cb1 | ||
|
|
e918889c3b | ||
|
|
e7d7ca48e4 | ||
|
|
c8baabf8d9 | ||
|
|
70a2ebb671 | ||
|
|
90a657c0fb | ||
|
|
2611a2e067 | ||
|
|
442c4f5eb5 | ||
|
|
85b4e23d31 | ||
|
|
13c23141ee | ||
|
|
9bdd7d5564 | ||
|
|
9bb2399e47 | ||
|
|
b34d87a209 | ||
|
|
cdd74c8f20 | ||
|
|
0bd3545a34 | ||
|
|
4b26317be8 | ||
|
|
09e3c7bec6 | ||
|
|
f097c8bb92 | ||
|
|
1a5ff4772d | ||
|
|
c402d90256 | ||
|
|
36789bd3b5 | ||
|
|
e6a60b879e | ||
|
|
bd737a82bf | ||
|
|
0064a44031 | ||
|
|
adc300d636 | ||
|
|
1bf252ad7e | ||
|
|
73dd4677da | ||
|
|
9a4b73e819 | ||
|
|
8fda2f4955 | ||
|
|
bbf45ea979 | ||
|
|
b3309f7b4e | ||
|
|
59f9ccdbdc | ||
|
|
231ccb1e43 | ||
|
|
8841464f84 | ||
|
|
04e2869953 | ||
|
|
845d1d9fb3 | ||
|
|
94f64a6e3f | ||
|
|
f0d90db640 | ||
|
|
5200917e0a | ||
|
|
8e485c3cab | ||
|
|
019a5024f9 | ||
|
|
ad17df8653 | ||
|
|
9b26d0f60a | ||
|
|
e1649d0361 | ||
|
|
57b804f3a3 | ||
|
|
396caabdaa | ||
|
|
a25350aa60 | ||
|
|
d4e76fb42c | ||
|
|
edad4ac025 | ||
|
|
26b7abf893 | ||
|
|
31d50cf67e | ||
|
|
d0e6a78f72 | ||
|
|
611c5f2631 | ||
|
|
1d27c22085 | ||
|
|
6b0e6ec02a | ||
|
|
63d0d138e6 | ||
|
|
7a49d55736 | ||
|
|
775faa9d79 | ||
|
|
ebf16393fc | ||
|
|
4cc1e8429e | ||
|
|
c41e95dc3b | ||
|
|
c599847aea | ||
|
|
79c8281a6b | ||
|
|
35cd354353 | ||
|
|
22691450af | ||
|
|
bafd17d029 | ||
|
|
a8710bacc9 | ||
|
|
dadb6b86c3 | ||
|
|
d97ccf0cfb | ||
|
|
11c616715d | ||
|
|
8ab3055223 | ||
|
|
1b7e144a6e | ||
|
|
d5965bf1d7 | ||
|
|
15e59ef65f | ||
|
|
a221ebfab8 | ||
|
|
49010a70a0 | ||
|
|
c60f8776f4 | ||
|
|
4f3265f8b6 | ||
|
|
8b246e64a6 | ||
|
|
335b6bc5c8 | ||
|
|
44c4514622 | ||
|
|
120873c7d6 | ||
|
|
5251d85fef | ||
|
|
6b42620ad3 | ||
|
|
e9770e17f6 | ||
|
|
eb566ca9db | ||
|
|
53f8abc311 | ||
|
|
626d52b15a | ||
|
|
8f79a4c34b | ||
|
|
ba304fcd00 | ||
|
|
64bb993ffa | ||
|
|
ab986677dc | ||
|
|
54344a5b54 | ||
|
|
4e04461f80 | ||
|
|
4c5f19579c | ||
|
|
a3bb108b33 | ||
|
|
037ed7e257 | ||
|
|
d6eb0d927a | ||
|
|
7fc268e518 | ||
|
|
83456164c7 | ||
|
|
bbd100c544 | ||
|
|
47ed699525 | ||
|
|
0e0d8c8962 | ||
|
|
a667b46316 | ||
|
|
af37b2b202 | ||
|
|
f6ef6d45ff | ||
|
|
a6c81bb071 | ||
|
|
2ccf758c8e | ||
|
|
1fcada1b07 | ||
|
|
08c5d6e7dd | ||
|
|
99d68bc354 | ||
|
|
1eb192db72 | ||
|
|
76e82ba159 | ||
|
|
c669a8e55f | ||
|
|
024efd3be7 | ||
|
|
ceeb55acf2 | ||
|
|
c75c0e9ed1 | ||
|
|
c59d19781f | ||
|
|
2e0077adec | ||
|
|
665aa4a226 | ||
|
|
231480e775 | ||
|
|
19ea5de1bf | ||
|
|
4cd24b87ff | ||
|
|
63f9137812 | ||
|
|
f6348123db | ||
|
|
eab3d8d938 | ||
|
|
2f746cb262 | ||
|
|
825ba99a08 | ||
|
|
0e3ed0fea0 | ||
|
|
a2814e19c3 | ||
|
|
87215506fe | ||
|
|
009428c72f | ||
|
|
b2b07ab920 | ||
|
|
7c2e456ab0 | ||
|
|
cb96ad95c4 | ||
|
|
b0ca61eff5 | ||
|
|
c09f71e9a9 | ||
|
|
3d2c9836e2 | ||
|
|
47e3fc90f2 | ||
|
|
841fca17cd | ||
|
|
739a2ce39e | ||
|
|
1a499f61fd | ||
|
|
0ab66e175a | ||
|
|
11a79da01a | ||
|
|
2a45142644 | ||
|
|
663f86ccda | ||
|
|
867c319a1e | ||
|
|
5cce4713a3 | ||
|
|
1e29f03c03 | ||
|
|
de45fe62cc | ||
|
|
12102fdfdd | ||
|
|
954a86b23d | ||
|
|
0f5e16f7d8 | ||
|
|
21f3661c8d | ||
|
|
821b175391 | ||
|
|
a022f90cef | ||
|
|
529223a43e | ||
|
|
b8ebe6c1bc | ||
|
|
0e3025eb79 | ||
|
|
0905cc4813 | ||
|
|
0d20305a27 | ||
|
|
71c99dad5a | ||
|
|
534a87b254 | ||
|
|
72ba59e95b | ||
|
|
92098cb60f | ||
|
|
d197636eda | ||
|
|
5cd879484a | ||
|
|
26547f48b6 | ||
|
|
7fa19917ff | ||
|
|
c9c0d0d888 | ||
|
|
e515163a90 | ||
|
|
6310e9a39d | ||
|
|
3fedf8a053 | ||
|
|
1ccef6cb26 | ||
|
|
d0b00887b4 | ||
|
|
d3609d04e4 | ||
|
|
2fdc9a2308 | ||
|
|
8470525f99 | ||
|
|
5800905f01 | ||
|
|
75fb5170c1 | ||
|
|
819147246f | ||
|
|
02210fb827 | ||
|
|
41d118b99b | ||
|
|
bd1192d716 | ||
|
|
56837256fa | ||
|
|
9e2b5202c9 | ||
|
|
be4fc15cb0 | ||
|
|
ed31ed206d | ||
|
|
5bf0f3b562 | ||
|
|
3a3b2f0771 | ||
|
|
fabc162481 | ||
|
|
9ff3e94870 | ||
|
|
311dc7610f | ||
|
|
1dd0f546ce | ||
|
|
3b682f5811 | ||
|
|
43d885b922 | ||
|
|
fc6a58fd5d | ||
|
|
f7ff9d662d | ||
|
|
1d6be7ae52 | ||
|
|
0050a8f4ad | ||
|
|
b8b813e605 | ||
|
|
c79f9ab2a0 | ||
|
|
336fdf074a | ||
|
|
5947ae9308 | ||
|
|
f49874c10e | ||
|
|
2327e812a8 | ||
|
|
c00a1e41d6 | ||
|
|
88a6ab3e11 | ||
|
|
a25c43f340 | ||
|
|
ebe952e005 | ||
|
|
8416e4e1cd | ||
|
|
09f57db753 | ||
|
|
6a6d836f9e | ||
|
|
fe0216aa3a | ||
|
|
d190aa114b | ||
|
|
15eaf18ec3 | ||
|
|
14edb6c18e | ||
|
|
71a2b46d14 | ||
|
|
37f014ff74 | ||
|
|
ae697bc06a | ||
|
|
135cf1d3df | ||
|
|
1d47f36573 | ||
|
|
d582e4508f | ||
|
|
e81ebc4460 | ||
|
|
924c6f5e7d | ||
|
|
36ccb5a962 | ||
|
|
cb35387fa3 | ||
|
|
b9ae32a901 | ||
|
|
aae8a4f771 | ||
|
|
c2f8eec411 | ||
|
|
ffb8fe4c1b | ||
|
|
933548dd79 | ||
|
|
d1039cbcb7 | ||
|
|
db2b630ed7 | ||
|
|
9831b23a5e | ||
|
|
9eaa0f4eb5 | ||
|
|
dbe01cb15e | ||
|
|
704dd518cb | ||
|
|
2776e4c10a | ||
|
|
454cc5a0c8 | ||
|
|
cb42124eb2 | ||
|
|
a65415a276 | ||
|
|
3d3e6d0961 | ||
|
|
ebd474eaef | ||
|
|
55bf922edb | ||
|
|
6878d4c0de | ||
|
|
3c28bc6b97 | ||
|
|
7404ee058f | ||
|
|
26fb2cf9fb | ||
|
|
1f8fddd3f4 | ||
|
|
b22d16a2b6 | ||
|
|
a6638e56fe | ||
|
|
a602dfdf04 | ||
|
|
0f11e81c23 | ||
|
|
9e7dc8b1e9 | ||
|
|
58c90f4888 | ||
|
|
d7d8fc2314 | ||
|
|
c191489891 | ||
|
|
183f033c7e | ||
|
|
bdbb1f8bbf | ||
|
|
12d1d1acae | ||
|
|
96d4ae51e3 | ||
|
|
cbbcaec957 | ||
|
|
10c10bb104 | ||
|
|
11770b59df | ||
|
|
9ab9a6c9e5 | ||
|
|
23572a1272 | ||
|
|
0899dea56f | ||
|
|
828c1eb5a2 | ||
|
|
62cf66b8ec | ||
|
|
52227a8e29 | ||
|
|
82f7c3f4b4 | ||
|
|
c10e84f68a | ||
|
|
5fb914fbc7 | ||
|
|
69b96edbde | ||
|
|
8f4fd6f214 | ||
|
|
10d2ef334f | ||
|
|
e7bc290e15 | ||
|
|
f17bb54e6f | ||
|
|
fd2f7e8c9d | ||
|
|
78cc010560 | ||
|
|
f124207072 | ||
|
|
e90ecd472c | ||
|
|
197f597d1c | ||
|
|
0a2c19a186 | ||
|
|
3c57c2e593 | ||
|
|
93c6a4f122 | ||
|
|
4fe9cc5523 | ||
|
|
3713d0f054 | ||
|
|
243baf5b5e | ||
|
|
40bcf6b94b | ||
|
|
e5c0e50c8c | ||
|
|
a6da438ae7 | ||
|
|
55b872ee03 | ||
|
|
cbbf673f4d | ||
|
|
804b9531f7 | ||
|
|
df04a6dc28 | ||
|
|
c50642021b | ||
|
|
e179b46140 | ||
|
|
9cdbb609eb | ||
|
|
3340e0ba9f | ||
|
|
24f7984d82 | ||
|
|
4cee246281 | ||
|
|
0ae7b11776 | ||
|
|
02576302c9 | ||
|
|
6dec3c9052 | ||
|
|
08f27c1ec2 | ||
|
|
d5a0ac8da8 | ||
|
|
35bfff878d | ||
|
|
a504fd9298 | ||
|
|
0bd5603dd0 | ||
|
|
a2ca00e3e8 | ||
|
|
59aa1cac35 | ||
|
|
2c014caa72 | ||
|
|
ffde5ddfb3 | ||
|
|
7968518f9c | ||
|
|
13abe74a31 | ||
|
|
216b5964d7 | ||
|
|
759ed2ad36 | ||
|
|
51d5c1e19f | ||
|
|
08e8a570b8 | ||
|
|
e0c21b4b85 | ||
|
|
bf8182f19b | ||
|
|
8b4674b87f | ||
|
|
2d52493b89 | ||
|
|
bf6f342ffd | ||
|
|
85bb5348c0 | ||
|
|
674e842861 | ||
|
|
eab3beaf2a | ||
|
|
7b70117155 | ||
|
|
a043eaec81 | ||
|
|
32277688af | ||
|
|
88279c55e0 | ||
|
|
932b71f345 | ||
|
|
9021d52096 | ||
|
|
dc03d04658 | ||
|
|
2dbd851fe7 | ||
|
|
17f060316d | ||
|
|
17dbbecf5e | ||
|
|
6cca678ece | ||
|
|
66e4a1bb61 | ||
|
|
b4a42a1681 | ||
|
|
404778089b | ||
|
|
1883b407a6 | ||
|
|
80d64574a6 | ||
|
|
6e0372dcee | ||
|
|
90365940ff | ||
|
|
84d139bb25 | ||
|
|
287b9b262b | ||
|
|
60c6777e2d | ||
|
|
ad6b51f76f | ||
|
|
5eee165377 | ||
|
|
178d65d12c | ||
|
|
a9a49a2d6f | ||
|
|
a71ea44753 | ||
|
|
4e6b4b2e0a | ||
|
|
9cabfe7570 | ||
|
|
ad07c5b472 | ||
|
|
c7cc250b1e | ||
|
|
8630b418d1 | ||
|
|
a893455097 | ||
|
|
b09be12a37 | ||
|
|
e69bcd1970 | ||
|
|
628770b6c1 | ||
|
|
2adccdbaf3 | ||
|
|
103b431780 | ||
|
|
a889fc262a | ||
|
|
14ee6f1b5b | ||
|
|
20a2f5f185 | ||
|
|
dfc415e3a8 | ||
|
|
0a3effc6ba | ||
|
|
b5b1d5f88a | ||
|
|
c7c43939ce | ||
|
|
9689b480a0 | ||
|
|
e8a5d09b97 | ||
|
|
f686e9e875 | ||
|
|
b0dda8d3ae | ||
|
|
af9b778f63 | ||
|
|
87313d0616 | ||
|
|
944a2cb8dc | ||
|
|
fadba0f36a | ||
|
|
ad56ca0f08 | ||
|
|
ea8a9a0741 | ||
|
|
2e0fc1585d | ||
|
|
baacddd324 | ||
|
|
da2ab68fcc | ||
|
|
91e60c1007 | ||
|
|
f3845be39f | ||
|
|
dd74759adc | ||
|
|
28cd8245c4 | ||
|
|
564afd96f2 | ||
|
|
78e294e98e | ||
|
|
9a4da9d7f0 | ||
|
|
3819311a6d | ||
|
|
0c89285033 | ||
|
|
7106ffceb3 | ||
|
|
c589e24aeb | ||
|
|
f33ae5e021 | ||
|
|
b5356a4115 | ||
|
|
30c95ac04e | ||
|
|
7105e23781 | ||
|
|
a702a45086 | ||
|
|
68cf58e9da | ||
|
|
cf2c3b944e | ||
|
|
bf54ed90a7 | ||
|
|
8e8f396c46 | ||
|
|
f1f4606cc6 | ||
|
|
db5bd3cd36 | ||
|
|
b29d8f617c | ||
|
|
50487af6a7 | ||
|
|
c2b7ddc76b | ||
|
|
6b29460bdf | ||
|
|
e14719b021 | ||
|
|
1ca039f796 | ||
|
|
45be0d7aa8 | ||
|
|
470da2ee15 | ||
|
|
f81fcb4e61 | ||
|
|
60dca97603 | ||
|
|
7a98e5e7d4 | ||
|
|
e30f9854b7 | ||
|
|
b85ed63ff6 | ||
|
|
dc06502744 | ||
|
|
75f198c8a0 | ||
|
|
00a507ff97 | ||
|
|
7662889237 | ||
|
|
2d4e72805a | ||
|
|
308c7197c7 | ||
|
|
014d22add9 | ||
|
|
8ea2ff4ae8 | ||
|
|
84c4522ff2 | ||
|
|
4bd9535219 | ||
|
|
2201060888 | ||
|
|
b824c394ab | ||
|
|
3464e296c3 | ||
|
|
6fde436865 | ||
|
|
a0a7e93707 | ||
|
|
85b06123c6 | ||
|
|
321782aa26 | ||
|
|
49f76ffe76 | ||
|
|
a019f4d80e | ||
|
|
1923d5cb4b | ||
|
|
f8b794ec1c | ||
|
|
17b1deb95b | ||
|
|
59b3fe70b6 | ||
|
|
2dbf192558 | ||
|
|
4a4b2938a6 | ||
|
|
51fe76cca3 | ||
|
|
1b49399c46 | ||
|
|
670271f257 | ||
|
|
d484392518 | ||
|
|
4a9bc53558 | ||
|
|
c732cec62f | ||
|
|
f5045b9759 | ||
|
|
58b368552f | ||
|
|
9dbf7b3bad | ||
|
|
bd7f9856f2 | ||
|
|
402de0d569 | ||
|
|
e2ed87a1ab | ||
|
|
eaf4bbe6d3 | ||
|
|
206c8abdf5 | ||
|
|
8c7fd77762 | ||
|
|
5436253ffd | ||
|
|
4dbcade830 | ||
|
|
b7963ebfa0 | ||
|
|
24cf17f68b | ||
|
|
17b6181b5d | ||
|
|
234a335425 | ||
|
|
44b8370f63 | ||
|
|
33c10365f0 | ||
|
|
a8f6dab672 | ||
|
|
a61ef0fa1c | ||
|
|
da2e56a8cb | ||
|
|
dae15e26d8 | ||
|
|
502c3b873c | ||
|
|
40db956766 | ||
|
|
1663cc6ce5 | ||
|
|
0f95bd6dc6 | ||
|
|
9ef96fccdd | ||
|
|
661e66d7fd | ||
|
|
4b11b5477e | ||
|
|
d8b05ee361 | ||
|
|
a9d077a299 | ||
|
|
a93db065d6 | ||
|
|
dbaa6eb3b2 | ||
|
|
0144be49bf | ||
|
|
a8c7146f96 | ||
|
|
84008edf45 | ||
|
|
bc74ca34d3 | ||
|
|
f85e7b5dec | ||
|
|
e64d49240f | ||
|
|
91b4ff16bd | ||
|
|
b03d35bc7a | ||
|
|
a86c5ba639 | ||
|
|
d92f25973b | ||
|
|
5ef79e5148 | ||
|
|
e3d2250e19 | ||
|
|
0d715c0bb4 | ||
|
|
9fa145dbc5 | ||
|
|
d528498d8d | ||
|
|
7f62d9b7aa | ||
|
|
8623bb5d60 | ||
|
|
6af13255f2 | ||
|
|
d8f6ed1370 | ||
|
|
57f497cf63 | ||
|
|
e1833ae032 | ||
|
|
1dbd9bda0c | ||
|
|
202fb08d09 | ||
|
|
d021bd26fd | ||
|
|
b5db67d23f | ||
|
|
97282075b0 | ||
|
|
f023a0799c | ||
|
|
b93a31f950 | ||
|
|
a80bb83ce9 | ||
|
|
ade75b4cbe | ||
|
|
c83cf64b80 | ||
|
|
bb77d0ae02 | ||
|
|
915a142315 | ||
|
|
f662841281 | ||
|
|
525535b7b8 | ||
|
|
f1c2e5be02 | ||
|
|
b80cae0776 | ||
|
|
77126d989a | ||
|
|
d10aec8f08 | ||
|
|
fe411e0a29 | ||
|
|
ce2e104b3e | ||
|
|
8ea0693d80 | ||
|
|
4d8f7d2af7 | ||
|
|
145c11d58e | ||
|
|
b1fef91c55 | ||
|
|
1b10e304d4 | ||
|
|
eb8bdd60c8 | ||
|
|
66fafd3981 | ||
|
|
2daa4f97e2 | ||
|
|
0172931bf9 | ||
|
|
0f82ad1109 | ||
|
|
d495369688 | ||
|
|
bfffa750ca | ||
|
|
5050d8edb3 | ||
|
|
1c1b043a50 | ||
|
|
81d521c562 | ||
|
|
938c209869 | ||
|
|
0b8ec5c664 | ||
|
|
d1823af111 | ||
|
|
07f6b7545a | ||
|
|
6c00a9cf3b | ||
|
|
ce2fb9d7f2 | ||
|
|
c53d9adaba | ||
|
|
618333e257 | ||
|
|
8b70fee072 | ||
|
|
91bc5931fd | ||
|
|
15e25a16e4 | ||
|
|
5a1835c576 | ||
|
|
cf720e7105 | ||
|
|
79edca93ef | ||
|
|
8d52b0afdf | ||
|
|
b5ab175953 | ||
|
|
8a8d1d756e | ||
|
|
ec3e99c6ab | ||
|
|
c7e52e04c2 | ||
|
|
08f391fcd7 | ||
|
|
b8b7f10055 | ||
|
|
5dd0eaefe0 | ||
|
|
8a63ae05ef | ||
|
|
9e29238ed1 | ||
|
|
f601e02bf7 | ||
|
|
d38bb34aae | ||
|
|
e56d1cf5c6 | ||
|
|
3e99f760a4 | ||
|
|
4fb359be59 | ||
|
|
2ee8fa6480 | ||
|
|
e388e6cd76 | ||
|
|
f19cfa850f | ||
|
|
19e24871e6 | ||
|
|
a4158393ea | ||
|
|
6564e825ec | ||
|
|
f8a2b3489a | ||
|
|
15f9aa11db | ||
|
|
75a6353445 | ||
|
|
ab77949e64 | ||
|
|
7948e86a07 | ||
|
|
b8c681d1a5 | ||
|
|
6e6ed0eab3 | ||
|
|
3b7deb1328 | ||
|
|
1506a158f4 | ||
|
|
5b536f81e3 | ||
|
|
a9eff27f99 | ||
|
|
e4e12bdaa9 | ||
|
|
b240c26667 | ||
|
|
5216535278 | ||
|
|
fb2eb62f83 | ||
|
|
1a05a0adf2 | ||
|
|
d98a2e015e | ||
|
|
99b73d87f6 | ||
|
|
90d039b36b | ||
|
|
38c4b4e26e | ||
|
|
9cfe164633 | ||
|
|
8c97b64a55 | ||
|
|
4852895488 | ||
|
|
03379a6163 | ||
|
|
edcb84b4ae | ||
|
|
aa03e84c79 | ||
|
|
353fdda3e5 | ||
|
|
d629f42e4c | ||
|
|
ae1e6184c3 | ||
|
|
7a762e50c7 | ||
|
|
8058c8d2f6 | ||
|
|
d11118a159 | ||
|
|
f970e6be2d | ||
|
|
163eb51ff6 | ||
|
|
b18f0ee2d9 | ||
|
|
d58cb75e53 | ||
|
|
9dd917894d | ||
|
|
305c3b6a86 | ||
|
|
bd9e50f222 | ||
|
|
492f1d4b10 | ||
|
|
5105f6d4c1 | ||
|
|
64818025e8 | ||
|
|
aae3072209 | ||
|
|
4d2cd82d95 | ||
|
|
f99daeebe5 | ||
|
|
1f86b73414 | ||
|
|
e2391d1ad2 | ||
|
|
614e6c5756 | ||
|
|
b7a68a4245 | ||
|
|
a696d6c0a1 | ||
|
|
ebf429215b | ||
|
|
d898178985 | ||
|
|
974acaab90 | ||
|
|
ceac15ace9 | ||
|
|
33653eae81 | ||
|
|
e8c93a1d0c | ||
|
|
57dc1d29b3 | ||
|
|
396a1a423e | ||
|
|
477628ed70 | ||
|
|
dbabbb9385 | ||
|
|
6237a3691b | ||
|
|
98d686bc14 | ||
|
|
163e926d6b | ||
|
|
199fb1a8d6 | ||
|
|
d26fb2db05 | ||
|
|
e4b0d43b3d | ||
|
|
415102aa37 | ||
|
|
820b26a315 | ||
|
|
4c18b84455 | ||
|
|
ff53f8f8fe | ||
|
|
8b02d7f892 | ||
|
|
7dbcc3a58d | ||
|
|
0c6950d49c | ||
|
|
424c1ff90a | ||
|
|
399ae06a5c | ||
|
|
30e3a8cc0f | ||
|
|
2f22267a77 | ||
|
|
67e359e616 | ||
|
|
31dd1b74b8 | ||
|
|
3655639bda | ||
|
|
916501c3ff | ||
|
|
f05ff65b30 | ||
|
|
9d02be6a20 | ||
|
|
298c0ac866 | ||
|
|
1fb3a26bd7 | ||
|
|
f1640d7565 | ||
|
|
b2079f7263 | ||
|
|
6c14055006 | ||
|
|
2b7962805f | ||
|
|
08d4cf3648 | ||
|
|
e364b6286f | ||
|
|
8d6eb96c8f | ||
|
|
9466fc71a8 | ||
|
|
e6bdb37ca1 | ||
|
|
4145c87e55 | ||
|
|
58e1c2e535 | ||
|
|
62fb32a914 | ||
|
|
b448b8661d | ||
|
|
a24dade801 | ||
|
|
8b8b0fc7e4 | ||
|
|
f35ea9a815 | ||
|
|
85d3a95dec | ||
|
|
e12eeeafc6 | ||
|
|
4ef78c24e1 | ||
|
|
7da5e84a35 | ||
|
|
cd6fc89e38 | ||
|
|
919acff536 | ||
|
|
a5be8b9afc | ||
|
|
12c95159f2 | ||
|
|
13d80b41a6 | ||
|
|
831410e8f0 | ||
|
|
05d8e7b353 | ||
|
|
636ffe5fbb | ||
|
|
f2ca5f6a30 | ||
|
|
7286134382 | ||
|
|
276cbc2f03 | ||
|
|
69064e2ad0 | ||
|
|
7aa5142047 | ||
|
|
a1441c9f22 | ||
|
|
e8669a20bb | ||
|
|
cb7a1fe30e | ||
|
|
0c7593fa0e | ||
|
|
cfeeeb3cd4 | ||
|
|
634a8ee9c0 | ||
|
|
dc2ebdf8e2 | ||
|
|
32f749bcb1 | ||
|
|
336e2e756f | ||
|
|
be47787166 | ||
|
|
e8f200186d | ||
|
|
a898b2f405 | ||
|
|
2a619dfa93 | ||
|
|
b4625f1599 | ||
|
|
9cc94287f9 | ||
|
|
4d04094baa | ||
|
|
e8938ebde7 | ||
|
|
86918738e4 | ||
|
|
81a04ef861 | ||
|
|
8af68d7c65 | ||
|
|
67b13e5b1b | ||
|
|
88bfa5663f | ||
|
|
9cec77ecc1 | ||
|
|
4f7f713046 | ||
|
|
7c090ee775 | ||
|
|
a6e8e5cb82 | ||
|
|
e65dcc1d79 | ||
|
|
53569957fe | ||
|
|
b2fdc39670 | ||
|
|
7ffe02c28a | ||
|
|
f01c6bce4b | ||
|
|
70e32e0905 | ||
|
|
cc054054c2 | ||
|
|
8648b3af88 | ||
|
|
93ff3001c6 | ||
|
|
07409cde50 | ||
|
|
7bdc6c683b | ||
|
|
e2266d1ffd | ||
|
|
7b586cf5fb | ||
|
|
af6f83b193 | ||
|
|
e464f253da | ||
|
|
5452d158f2 | ||
|
|
0437e11a47 | ||
|
|
2e68dd6bef | ||
|
|
7b9993b84f | ||
|
|
674859b835 | ||
|
|
dd829c5d97 | ||
|
|
be0348b324 | ||
|
|
fc5b26dfc2 | ||
|
|
b0a4fb77be | ||
|
|
539ddcc51f | ||
|
|
cf08c1aba4 | ||
|
|
7671503562 | ||
|
|
22ffe4a079 | ||
|
|
a0a9d8fd65 | ||
|
|
8c676aa267 | ||
|
|
426c92fe33 | ||
|
|
b859cd6aec | ||
|
|
c4df524a06 | ||
|
|
b182372b83 | ||
|
|
c6db5f1def | ||
|
|
39bb3719c2 | ||
|
|
16daa7b55e | ||
|
|
11c5b0e8f1 | ||
|
|
05f45d36d1 | ||
|
|
722bec1a1a | ||
|
|
489ed81c84 | ||
|
|
4521a9917a | ||
|
|
73e6848ff4 | ||
|
|
e01af6a7ac | ||
|
|
acc1c0badc | ||
|
|
78938caaf3 | ||
|
|
565ef41eba | ||
|
|
a52881fb67 | ||
|
|
af3cecb2e1 | ||
|
|
a7081660f0 | ||
|
|
8cf8823d56 | ||
|
|
094609102d | ||
|
|
cefe679c98 | ||
|
|
67488a56f6 | ||
|
|
5259da41f2 | ||
|
|
7236412e48 | ||
|
|
d68843e4ec | ||
|
|
cc383f6b0e | ||
|
|
b5c6ecac3e | ||
|
|
76ae57b481 | ||
|
|
71eaaf007e | ||
|
|
516a080a5b | ||
|
|
2b8fbaf091 | ||
|
|
07079b519a | ||
|
|
6129fb4233 | ||
|
|
9f5c75d9db | ||
|
|
7578d18cab | ||
|
|
a9764bf034 | ||
|
|
131be583e5 | ||
|
|
dc49aaf94d | ||
|
|
d7b5f3f4db | ||
|
|
206758c803 | ||
|
|
b1c9bebce6 | ||
|
|
977dc1e5e4 | ||
|
|
fa3d4727f9 | ||
|
|
b1471c2bbd | ||
|
|
6e2e07ce98 | ||
|
|
a86c143160 | ||
|
|
3396a3d791 | ||
|
|
c21817e5e0 | ||
|
|
e23e0138a9 | ||
|
|
1126712d11 | ||
|
|
bc196bcd1a | ||
|
|
08a082a190 | ||
|
|
93d1b6de9a | ||
|
|
ac424fc584 | ||
|
|
96e090650d | ||
|
|
15deadaa1c | ||
|
|
d0376b53a8 | ||
|
|
e148cdea75 | ||
|
|
4c8d84d07b | ||
|
|
860b7701f3 | ||
|
|
6666712bc9 | ||
|
|
f862634143 | ||
|
|
e45959949c | ||
|
|
c6500dfe14 | ||
|
|
352ef98bc6 | ||
|
|
49a63f37c9 | ||
|
|
32e933b435 | ||
|
|
2750e0698d | ||
|
|
f929c8fb69 | ||
|
|
f79e6f253d | ||
|
|
f6f3040ad2 | ||
|
|
82a1f4d781 | ||
|
|
dc6674af4a | ||
|
|
0b421d5a62 | ||
|
|
6dff9cd781 | ||
|
|
3b43a8bc78 | ||
|
|
d65d77ef2f | ||
|
|
188b419910 | ||
|
|
5b1404ffac | ||
|
|
271065fe44 | ||
|
|
8047a36b70 | ||
|
|
cfcc8c73af | ||
|
|
974350ebee | ||
|
|
b9ef8ae2c9 | ||
|
|
ba543c95d7 | ||
|
|
09f00d1766 | ||
|
|
528b6cd096 | ||
|
|
f23b7205c6 | ||
|
|
4cfd6479ce | ||
|
|
f080a302b1 | ||
|
|
d6bde1aab0 | ||
|
|
7a53cae32f | ||
|
|
7e42e99019 | ||
|
|
87df281434 | ||
|
|
3d63eeb79e | ||
|
|
65bf9be580 | ||
|
|
da4b9684b4 | ||
|
|
c30c41a79b | ||
|
|
974adb78e2 | ||
|
|
95024aca10 | ||
|
|
d43d3340e4 | ||
|
|
e1d66d90ae | ||
|
|
d25af53dfb | ||
|
|
79510eb79c | ||
|
|
dba125ad59 | ||
|
|
7feb9a9f41 | ||
|
|
b3a969fb8a | ||
|
|
890fe132e1 | ||
|
|
790edc73c8 | ||
|
|
53eac88edb | ||
|
|
b20df3209f | ||
|
|
1d4b20bb79 | ||
|
|
bd35521f41 | ||
|
|
7f8707c4c0 | ||
|
|
745a3603fe | ||
|
|
dcd1d1cbd4 | ||
|
|
b2ca7f50ad | ||
|
|
cc068e9dc4 | ||
|
|
98996ba4b7 | ||
|
|
d41f9d8eee | ||
|
|
51f250cc78 | ||
|
|
302dcf67c2 | ||
|
|
80eea23b10 | ||
|
|
c5c180cb2e | ||
|
|
29beb9b75d | ||
|
|
61899ce092 | ||
|
|
038e093e68 | ||
|
|
ad8eccf83b | ||
|
|
a72b671b1c | ||
|
|
e1f8fa5090 | ||
|
|
177dfcb7e5 | ||
|
|
e1aa566ec8 | ||
|
|
bd35a73f5f | ||
|
|
686fa20945 | ||
|
|
7b29dca261 | ||
|
|
a2336be273 | ||
|
|
6923ddebe8 | ||
|
|
c9608f18a2 | ||
|
|
f10f93fd8f | ||
|
|
5eab70f935 | ||
|
|
2321abcf47 | ||
|
|
07dae9dd30 | ||
|
|
9c9eea0dac | ||
|
|
4572d3f43d | ||
|
|
5149357888 | ||
|
|
d3b716cfda | ||
|
|
be512a0333 | ||
|
|
cf12960507 | ||
|
|
29854311f9 | ||
|
|
1311fd343b | ||
|
|
9a5ad04a6e | ||
|
|
712cfbb8f4 | ||
|
|
7858b3e06a | ||
|
|
a2ddd493ce | ||
|
|
d4e1e90eef | ||
|
|
d6d9be0dab | ||
|
|
e95b919c1e | ||
|
|
4ead42aea2 | ||
|
|
02a553c79f | ||
|
|
a9c60137da | ||
|
|
5ff8b28a12 | ||
|
|
e9504baa12 | ||
|
|
1a71309b81 | ||
|
|
56c681bbd5 | ||
|
|
c7991c91b7 | ||
|
|
19c0c5c98e | ||
|
|
2c3766f4d5 | ||
|
|
0e5ae38c58 | ||
|
|
cfd9c11db6 | ||
|
|
becdf29120 | ||
|
|
8bd466a656 | ||
|
|
1f9b3ec699 | ||
|
|
8fdd0407db | ||
|
|
1c9b53eb25 | ||
|
|
de38c3b3e9 | ||
|
|
a88c2e94f9 | ||
|
|
5cadccb985 | ||
|
|
083b6988ee | ||
|
|
27b19ef29d | ||
|
|
8b4019d934 | ||
|
|
33d8482c4c | ||
|
|
42c1c20a76 | ||
|
|
92d5a4176e | ||
|
|
1c43b21fa8 | ||
|
|
e35d74be0b | ||
|
|
fb319c356d | ||
|
|
c8deecb4aa | ||
|
|
fe718e81e9 | ||
|
|
5eef0b409b | ||
|
|
4922586893 | ||
|
|
4261bdafff | ||
|
|
d1b31c6822 | ||
|
|
201d2e3c94 | ||
|
|
41db44bf35 | ||
|
|
6abcbd16c4 | ||
|
|
34543d5bf3 | ||
|
|
94d33a6daf | ||
|
|
d4c4f84c13 | ||
|
|
1cff18218e | ||
|
|
ba460584bd | ||
|
|
94c4b6c849 | ||
|
|
4805a377c4 | ||
|
|
3a96823506 | ||
|
|
ee27ef29d4 | ||
|
|
7c928ee78e | ||
|
|
faa17ca3de | ||
|
|
0ff7b33a48 | ||
|
|
4ac3cba065 | ||
|
|
dfc76b4a47 | ||
|
|
5c9fdb6b55 | ||
|
|
871664cd39 | ||
|
|
ad45bcf2cf | ||
|
|
228fefc121 | ||
|
|
3f2d193bf6 | ||
|
|
c522daf866 | ||
|
|
f2b8719223 | ||
|
|
ff9dfa28fe | ||
|
|
432d3b5f47 | ||
|
|
c41049ab94 | ||
|
|
487f151dd0 | ||
|
|
daa1185459 | ||
|
|
af59dd3d0d | ||
|
|
75b59322ad | ||
|
|
a83cb9150e | ||
|
|
508c262a4b | ||
|
|
bf45070ff1 | ||
|
|
27dddd3b1b | ||
|
|
9764c13fdd | ||
|
|
a9b336d90f | ||
|
|
a451600d8b | ||
|
|
ea3eff5d0d | ||
|
|
79b19a978f | ||
|
|
16e513cf98 | ||
|
|
b0ed74575f | ||
|
|
fca7e44aa0 | ||
|
|
81e46ccdf7 | ||
|
|
a313664b64 | ||
|
|
e4b1950dbe | ||
|
|
18b7b39b49 | ||
|
|
8aa9d3009b | ||
|
|
d8a64124f2 | ||
|
|
bb39d1709e | ||
|
|
6cd1007938 | ||
|
|
bafc5ad921 | ||
|
|
c50670fc7f | ||
|
|
cfdc37189b | ||
|
|
40d29acb15 | ||
|
|
7c4f1c309e | ||
|
|
9f69a992c8 | ||
|
|
215767ed9b | ||
|
|
bad33e6dc4 | ||
|
|
ae1de36ee0 | ||
|
|
277c567f54 | ||
|
|
315f2a345a | ||
|
|
219b155ede | ||
|
|
31b1096f4d | ||
|
|
efd27cb211 | ||
|
|
3012084184 | ||
|
|
2e3e1fa5fe | ||
|
|
731ac52564 | ||
|
|
22450e7430 | ||
|
|
a78fcb6308 | ||
|
|
8b65b6ee7c | ||
|
|
2a86ab5753 | ||
|
|
5785ad511b | ||
|
|
a21c859aba | ||
|
|
6529d01a4f | ||
|
|
ef4ccdfb99 | ||
|
|
c3c7aa9aef | ||
|
|
fed0d53425 | ||
|
|
aa1128d40c | ||
|
|
f88643ce65 | ||
|
|
980f1e45ef | ||
|
|
2ea3f6de4a | ||
|
|
c5f419e8da | ||
|
|
34d8cf002b | ||
|
|
9306808263 | ||
|
|
9c7f522c82 | ||
|
|
d7af7d15a0 | ||
|
|
ef69e31ad1 | ||
|
|
d126380282 | ||
|
|
f5beeb9ca1 | ||
|
|
2793dae547 | ||
|
|
cdb4c0a3cf | ||
|
|
4aa684e186 | ||
|
|
ed01a4df61 | ||
|
|
9a3c5c17b7 | ||
|
|
757dc78bad | ||
|
|
88ad0ccc6c | ||
|
|
19bc530879 | ||
|
|
3fdb2e7698 | ||
|
|
edd083801f | ||
|
|
9484ff3cbb | ||
|
|
26706acf6a | ||
|
|
b3fa14f017 | ||
|
|
99a3132023 | ||
|
|
9fb7cd881f | ||
|
|
54896bf044 | ||
|
|
8799cd3e8d | ||
|
|
30a6f7f94a | ||
|
|
7c472943cf | ||
|
|
21d7e91493 | ||
|
|
439ad5596f | ||
|
|
77669cfcc7 | ||
|
|
d7e93d8229 | ||
|
|
06860a51e6 | ||
|
|
6765f4bb5d | ||
|
|
569f4daac1 | ||
|
|
fd6dc611bb | ||
|
|
21eaf88fd9 | ||
|
|
f8f8cd0124 | ||
|
|
df8e4d2b9d | ||
|
|
ec80404eed | ||
|
|
5c5287ea23 | ||
|
|
5394599a9d | ||
|
|
d756558303 | ||
|
|
3ccf70e5cd | ||
|
|
f98f1db441 | ||
|
|
780aa8759d | ||
|
|
d0db7f5ce3 | ||
|
|
c27e231f53 | ||
|
|
60b72ed981 | ||
|
|
e0f3995e85 | ||
|
|
9c3fe7d4e5 | ||
|
|
48712ac13f | ||
|
|
97bdaf4788 | ||
|
|
7807859975 | ||
|
|
33a4692c06 | ||
|
|
a2ec25fbd4 | ||
|
|
95a84f8e62 | ||
|
|
682af34d53 | ||
|
|
6af4ead706 | ||
|
|
b374023ddb | ||
|
|
c9b1098236 | ||
|
|
477158e54a | ||
|
|
4397eaed86 | ||
|
|
079f334bcc | ||
|
|
9bb65609e2 | ||
|
|
8a5274de61 | ||
|
|
72e89ba4e0 | ||
|
|
40cf942ddd | ||
|
|
b679cb41d9 | ||
|
|
39bc370dff | ||
|
|
70ff592bb7 | ||
|
|
94c3fa464d | ||
|
|
8d1290e2c4 | ||
|
|
b64e0a026d | ||
|
|
b2e111fc14 | ||
|
|
4709e6fc25 | ||
|
|
5efd926f22 | ||
|
|
544ec16830 | ||
|
|
01d1d5d284 | ||
|
|
95bcc29d59 | ||
|
|
08217d4e99 | ||
|
|
c7f554d22b | ||
|
|
0b2e82f9ca | ||
|
|
01a9556b9c | ||
|
|
6b6aa54b99 | ||
|
|
31e2962f9a | ||
|
|
e8fc4eaa30 | ||
|
|
dc335f0016 | ||
|
|
6a6b800158 | ||
|
|
ae40214f46 | ||
|
|
34b0a09ed6 | ||
|
|
6ac1f2b67a | ||
|
|
aa22dc61ca | ||
|
|
116fed7284 | ||
|
|
75c923f00f | ||
|
|
07876dee81 | ||
|
|
f82574a472 | ||
|
|
d48b3a74fc | ||
|
|
a7f93ef864 | ||
|
|
4a5bd9033f | ||
|
|
c9294619e9 | ||
|
|
7af66185c4 | ||
|
|
a88dc82224 | ||
|
|
77f738f79f | ||
|
|
fb29f8b0dd | ||
|
|
605ee88041 | ||
|
|
e5974c600a | ||
|
|
9bfce21d23 | ||
|
|
e4664ad458 | ||
|
|
88096589ac | ||
|
|
cb2a265373 | ||
|
|
2aca295851 | ||
|
|
543725dd42 | ||
|
|
bff88e9f6a | ||
|
|
f2aba8b11d | ||
|
|
4d0ee8736b | ||
|
|
c56cc7be73 | ||
|
|
ae4a71f9a5 | ||
|
|
4823036995 | ||
|
|
a57ead9f7c | ||
|
|
06300de9f8 | ||
|
|
2f4e17da51 | ||
|
|
f4b9501388 | ||
|
|
9502fa3f5a | ||
|
|
9df052a435 | ||
|
|
a4ea8a46a8 | ||
|
|
e92910d4a0 | ||
|
|
15aa1ce24f | ||
|
|
f86ed70238 | ||
|
|
85556d9c4c | ||
|
|
0d6499702d | ||
|
|
95808ba377 | ||
|
|
6d7c84226a | ||
|
|
71069a8cc9 | ||
|
|
a4aec30035 | ||
|
|
79191cf7a3 | ||
|
|
5c71c3949e | ||
|
|
f6f820c43d | ||
|
|
721edc5e57 | ||
|
|
200e00e2d6 | ||
|
|
a492b5bdf9 | ||
|
|
0863976859 | ||
|
|
4ebde92a06 | ||
|
|
81bec371bf | ||
|
|
9c8d757ebf | ||
|
|
e27e014b81 | ||
|
|
aa842dbc83 | ||
|
|
95bf7b1fe5 | ||
|
|
3aa063d475 | ||
|
|
dc07313869 | ||
|
|
4085f58b4e | ||
|
|
b89d194863 | ||
|
|
e5ed52b79e | ||
|
|
a085bbb51d | ||
|
|
de7d1a82c3 | ||
|
|
ebaa181fd3 | ||
|
|
a34041c2dd | ||
|
|
3799a107a2 | ||
|
|
ad4159fd2c | ||
|
|
e797d0e5ed | ||
|
|
7e3b81add9 | ||
|
|
33cb705b07 | ||
|
|
73faa02795 | ||
|
|
21c0c1ed5d | ||
|
|
fa4bb8ae18 | ||
|
|
d8b9907e32 | ||
|
|
ce7bcbcaf5 | ||
|
|
5c02806a76 | ||
|
|
dee6f6af7e | ||
|
|
2634edd842 | ||
|
|
82d5d34174 | ||
|
|
56fe3637f3 | ||
|
|
6c8d8d597b | ||
|
|
a80a5d3dad | ||
|
|
5a709a937d | ||
|
|
81ade9f48e | ||
|
|
bf5050945b | ||
|
|
f41fff1e3e | ||
|
|
3594b344fd | ||
|
|
b44da10e27 | ||
|
|
7246dd083f | ||
|
|
3948b11998 | ||
|
|
b29055a825 | ||
|
|
ac5faa90ee | ||
|
|
1e430c8e57 | ||
|
|
29473045f8 | ||
|
|
cd66ce1d28 | ||
|
|
e067079f8a | ||
|
|
0af70e0115 | ||
|
|
0d7ffcb0a5 | ||
|
|
308be25c24 | ||
|
|
da98c611e1 | ||
|
|
5df476a7d0 | ||
|
|
2053749b54 | ||
|
|
b7fd933499 | ||
|
|
6d4f626cd6 | ||
|
|
288e1c8742 | ||
|
|
77ab2fddf5 | ||
|
|
59cd22625a | ||
|
|
0deedbecf5 | ||
|
|
5069bc2950 | ||
|
|
113a421d0f | ||
|
|
ac73c0e3ff | ||
|
|
7ef79635a6 | ||
|
|
274ebd6855 | ||
|
|
bf6b30c971 | ||
|
|
91978e6ecf | ||
|
|
7dca36409d | ||
|
|
434ac21103 | ||
|
|
1032d0eb45 | ||
|
|
63316be535 | ||
|
|
c503aaca1d | ||
|
|
b655e0ad70 | ||
|
|
08baff9e64 | ||
|
|
d468bac551 | ||
|
|
86c881970e | ||
|
|
cb167da854 | ||
|
|
2d832ff5ea | ||
|
|
095bd04634 | ||
|
|
8b508456b2 | ||
|
|
d37022b116 | ||
|
|
6281b5eee2 | ||
|
|
7878b542a5 | ||
|
|
4bf1fa9f86 | ||
|
|
8e204409f6 | ||
|
|
ebaf92a2f0 | ||
|
|
867b7081f6 | ||
|
|
4a2dcbc1f7 | ||
|
|
7da3ba8c61 | ||
|
|
ecf53b1285 | ||
|
|
7e1b85d023 | ||
|
|
bd287cb749 | ||
|
|
8cf5bd9c0a | ||
|
|
30577b677a | ||
|
|
99816083ef | ||
|
|
38daad6d9b | ||
|
|
28a7377f97 | ||
|
|
28a70ccc8e | ||
|
|
48f4e51dc5 | ||
|
|
92240e84a6 | ||
|
|
e5ef0eaa6a | ||
|
|
ff81aa6abc | ||
|
|
10e36e96b6 | ||
|
|
481ef70976 | ||
|
|
f4448635d1 | ||
|
|
ee5fb1a83e | ||
|
|
565c8b13fa | ||
|
|
b926d86568 | ||
|
|
460ea5a479 | ||
|
|
c1b33dc0c4 | ||
|
|
676d379e29 | ||
|
|
9cda088645 | ||
|
|
9fbae7f07e | ||
|
|
81a453955e | ||
|
|
920a8995d3 | ||
|
|
b47035ef43 | ||
|
|
58e9ebc37b | ||
|
|
4a0a6c80fc | ||
|
|
901e45d672 | ||
|
|
591955af1d | ||
|
|
b2111b20d1 | ||
|
|
f23e0d7a03 | ||
|
|
456ade5845 | ||
|
|
bc5194a494 | ||
|
|
31aaf80f00 | ||
|
|
7fafb6ee9c | ||
|
|
1a93719e3f | ||
|
|
089f016633 | ||
|
|
951ba62621 | ||
|
|
fdc0e3f59f | ||
|
|
9f1e9f1899 | ||
|
|
65e614f00f | ||
|
|
01f26451d0 | ||
|
|
c74c6474a3 | ||
|
|
74da439b08 | ||
|
|
09727b80e6 | ||
|
|
23b31c7c26 | ||
|
|
5f19bb8699 | ||
|
|
4a85dcfbb3 | ||
|
|
ecb4dc17b0 | ||
|
|
e7958fb1ed | ||
|
|
a13bb9e930 | ||
|
|
c929033388 | ||
|
|
4e3a97a6f1 | ||
|
|
8e7569c8f3 | ||
|
|
07db91c7ce | ||
|
|
5e9c8eb39e | ||
|
|
ef6b95722d | ||
|
|
3079771f29 | ||
|
|
9745d0ed89 | ||
|
|
135c8f2c24 | ||
|
|
2ac376d582 | ||
|
|
ff065378ab | ||
|
|
cc1d0a6e38 | ||
|
|
d76ba3abc9 | ||
|
|
efbc7563ac | ||
|
|
3c78f9bf04 | ||
|
|
2882b58fcf | ||
|
|
6c276b9b03 | ||
|
|
14bd9720e9 | ||
|
|
364947d1f9 | ||
|
|
cc6a4c4de9 | ||
|
|
3ee1bd488a | ||
|
|
eb34fd64d1 | ||
|
|
fbabf0a283 | ||
|
|
2f0e410b80 | ||
|
|
8f37226b76 | ||
|
|
1d629209b4 | ||
|
|
2a1c5c9a6a | ||
|
|
895abd09be | ||
|
|
3992d9d5f7 | ||
|
|
697250e361 | ||
|
|
9db813b6ee | ||
|
|
7ed6bfbf56 | ||
|
|
33cc9d54ff | ||
|
|
decbbd8817 | ||
|
|
6f4dbf799b | ||
|
|
2ee9d973e2 | ||
|
|
04c11ddc49 | ||
|
|
10835a6f82 | ||
|
|
061b219a9a | ||
|
|
ed89b60892 | ||
|
|
883906f441 | ||
|
|
a595c5dd6e | ||
|
|
f41d40c2c0 | ||
|
|
05db6b5bb3 | ||
|
|
c8ee67c3ba | ||
|
|
daca2b54bf | ||
|
|
2d6e2deb3d | ||
|
|
8d2446f314 | ||
|
|
2705f16e44 | ||
|
|
4ec7070ffa | ||
|
|
857ab52a5e | ||
|
|
4dd782014d | ||
|
|
80579a3ed0 | ||
|
|
07e80dd36e | ||
|
|
92d967f1e6 | ||
|
|
5cd5f77c6f | ||
|
|
0e347625e3 | ||
|
|
529d727189 | ||
|
|
e0320c63c1 | ||
|
|
6e4b6e543f | ||
|
|
84e0fa5756 | ||
|
|
95ce1b15ef | ||
|
|
7da114b0c9 | ||
|
|
94506ed726 | ||
|
|
2f8203abda | ||
|
|
115437fb6a | ||
|
|
0feae48464 | ||
|
|
ad8da7dce0 | ||
|
|
db426abbac | ||
|
|
7f4ebe560b | ||
|
|
2693e94417 | ||
|
|
8a0cb947f5 | ||
|
|
5d8b067ca0 | ||
|
|
54fc65a921 | ||
|
|
538f752fbe | ||
|
|
35f37381ae | ||
|
|
00c5c8f35a | ||
|
|
199d1d4c42 | ||
|
|
8c356d8154 | ||
|
|
44c42ade52 | ||
|
|
f796b578fc | ||
|
|
4522a2ef6e | ||
|
|
e5572975a5 | ||
|
|
7d3cbcd333 | ||
|
|
245aaa92a2 | ||
|
|
3664b140c4 | ||
|
|
009b76de24 | ||
|
|
1ced0dcd1e | ||
|
|
0a21485003 | ||
|
|
f0a3ea1348 | ||
|
|
5c7bdde088 | ||
|
|
c9277c9227 | ||
|
|
61e142bfad | ||
|
|
aa942b9d00 | ||
|
|
ce3acc4ec8 | ||
|
|
7c362879ed | ||
|
|
d3b65afb35 | ||
|
|
39a827aa2e | ||
|
|
719847c43e | ||
|
|
fe4f182606 | ||
|
|
0ef8af21b1 | ||
|
|
874bfa852b | ||
|
|
d33ca12b46 | ||
|
|
9aaaea48bd | ||
|
|
dc57442ba0 | ||
|
|
7bca4fb6af | ||
|
|
486878127d | ||
|
|
1316c2d14e | ||
|
|
b02274b35f | ||
|
|
f805ae53dc | ||
|
|
c199b63304 | ||
|
|
e3ffad8552 | ||
|
|
9f41f18d43 | ||
|
|
d416ab3642 | ||
|
|
d993b213a7 | ||
|
|
bd117d02e6 | ||
|
|
b39d023241 | ||
|
|
41df46dbed | ||
|
|
c6850c6c3d | ||
|
|
c7df5b9a3e | ||
|
|
cd09faa2ac | ||
|
|
2cdadf3a6c | ||
|
|
bef1a378ca | ||
|
|
b9412ac09b | ||
|
|
2fc2b0cb1e | ||
|
|
e7b06496d2 | ||
|
|
806f6f3c5e | ||
|
|
1dbf2e909f | ||
|
|
6e4be76664 | ||
|
|
028f4ecb08 | ||
|
|
a6988db8ce | ||
|
|
2ef9b22385 | ||
|
|
2c54b0caf0 | ||
|
|
9430c987e1 | ||
|
|
7fac6ca9dd | ||
|
|
839ebfb350 | ||
|
|
ff68fa15d1 | ||
|
|
a58276ff45 | ||
|
|
cef870328c | ||
|
|
772d722972 | ||
|
|
23b5de06b1 | ||
|
|
00001c758d | ||
|
|
0d4a4ad280 | ||
|
|
3c57f39ae0 | ||
|
|
95379cb951 | ||
|
|
cd0f71bdce | ||
|
|
8406bd4c9f | ||
|
|
3caeae8ea6 | ||
|
|
6ac07fb17b | ||
|
|
787e236133 | ||
|
|
f05dcb6965 | ||
|
|
fd0a41cfaf | ||
|
|
a1cafb1d0c | ||
|
|
b9036ad829 | ||
|
|
63d693f21b | ||
|
|
7a0ec76d25 | ||
|
|
9130da9dca | ||
|
|
108dd92163 | ||
|
|
d706793e56 | ||
|
|
a6bf2abaff | ||
|
|
dec2c1e84c | ||
|
|
be8c453cac | ||
|
|
663a6bf3ad | ||
|
|
14c684a866 | ||
|
|
ba4b8f84cc | ||
|
|
f3e6dda69b | ||
|
|
e06acbe738 | ||
|
|
256023dc58 | ||
|
|
ffcc86805a | ||
|
|
3903a37b64 | ||
|
|
4fa931f631 | ||
|
|
797c95877e | ||
|
|
e183764e51 | ||
|
|
a197378b7c | ||
|
|
31d6666729 | ||
|
|
2247be5026 | ||
|
|
59c7baaab8 | ||
|
|
dee4e05e17 | ||
|
|
51e6a88d37 | ||
|
|
f793e276bc | ||
|
|
aeabb16808 | ||
|
|
21fe9dcd68 | ||
|
|
b1bcbc1671 | ||
|
|
b6f35e00d6 | ||
|
|
3e3f85aaeb | ||
|
|
7911f9019a | ||
|
|
acc7787b1a | ||
|
|
7f231ef24f | ||
|
|
c94e38b297 | ||
|
|
dc5ac7025d | ||
|
|
50ddca2e42 | ||
|
|
79387262e3 | ||
|
|
c7833d622a | ||
|
|
62a14a3ab0 | ||
|
|
bcf63b6700 | ||
|
|
cbafa55450 | ||
|
|
ca07dcab66 | ||
|
|
30c2371b9a | ||
|
|
2ede940aa7 | ||
|
|
b39305c8d8 | ||
|
|
ce4c6d1925 | ||
|
|
057bd9d15c | ||
|
|
c1cc8c5060 | ||
|
|
94e703a98b | ||
|
|
3d809b1faa | ||
|
|
550dfb3459 | ||
|
|
d6940aad7e | ||
|
|
a13c037fd7 | ||
|
|
5b38bf3b17 | ||
|
|
b690370557 | ||
|
|
a9ea62abc2 | ||
|
|
b435b14497 | ||
|
|
4a89edec01 | ||
|
|
7211869636 | ||
|
|
a0b4517074 | ||
|
|
60d7cbb318 | ||
|
|
c7cf5b1c2c | ||
|
|
04d55be625 | ||
|
|
bdea9c6833 | ||
|
|
f53dab1baa | ||
|
|
5abc6a3034 | ||
|
|
93d8b5a419 | ||
|
|
b77854e421 | ||
|
|
a41afb17ad | ||
|
|
640f75da8d | ||
|
|
dd24af93ea | ||
|
|
6f59b58e19 | ||
|
|
2b2de2abdb | ||
|
|
4b54b71f53 | ||
|
|
37fd11a8cb | ||
|
|
4faa4db112 | ||
|
|
9d9ea1ec31 | ||
|
|
69fb887e63 | ||
|
|
40e2375b3e | ||
|
|
ddbe6b2a75 | ||
|
|
f9e0cc40a4 | ||
|
|
65c313c0e4 | ||
|
|
b683abc89f | ||
|
|
20cc74b066 | ||
|
|
4fb0199a0f | ||
|
|
0ff69776ed | ||
|
|
aa01343df8 | ||
|
|
b011d1fa0e | ||
|
|
f3e159f2c6 | ||
|
|
dcc8db23b1 | ||
|
|
a5bff8674a | ||
|
|
e6fc06b346 | ||
|
|
fc2958538c | ||
|
|
13492a9d0b | ||
|
|
f04a0ca7f3 | ||
|
|
7db9fb36fa | ||
|
|
5882cc6051 | ||
|
|
87d01c8bde | ||
|
|
2056600a72 | ||
|
|
e8576c75b9 | ||
|
|
0f3d8e66df | ||
|
|
187dfcd126 | ||
|
|
258afdd26e | ||
|
|
0910b8f27d | ||
|
|
115f603d20 | ||
|
|
7250bfa1e1 | ||
|
|
f649ea05fd | ||
|
|
c5d1e5e9c6 | ||
|
|
00b08260e9 | ||
|
|
50dffed895 | ||
|
|
dfc1058839 | ||
|
|
313d1efb14 | ||
|
|
d78cf50211 | ||
|
|
61770dd5c7 | ||
|
|
1625e722a2 | ||
|
|
ee03abfd09 | ||
|
|
99b84af7b5 | ||
|
|
1b3d6401f3 | ||
|
|
c63366d0dc | ||
|
|
46556931cc | ||
|
|
5c597c4c1d | ||
|
|
dc241ded1b | ||
|
|
063ed7d844 | ||
|
|
211600135d | ||
|
|
b5fb4f5b52 | ||
|
|
b2e1fa993d | ||
|
|
0e56b693c1 | ||
|
|
caa6ee8290 | ||
|
|
5ce11ce3dd | ||
|
|
cf8cdc25c3 | ||
|
|
921ce9471e | ||
|
|
9cca299e6a | ||
|
|
68309e5f7b | ||
|
|
5fbca6fe48 | ||
|
|
d52f2adb2b | ||
|
|
dcf4983a7d | ||
|
|
47c1071f3f | ||
|
|
63ef2cc724 | ||
|
|
bdca1f4ff6 | ||
|
|
e7a54e69d1 | ||
|
|
258f7fb1ad | ||
|
|
8b842d920d | ||
|
|
4583616ea6 | ||
|
|
b3476818ee | ||
|
|
32059330dd | ||
|
|
a6d5f6ad91 | ||
|
|
c3a95db495 | ||
|
|
8ae5563a84 | ||
|
|
036bac7914 | ||
|
|
b12852bd08 | ||
|
|
689126c57d | ||
|
|
266217434f | ||
|
|
fc7db056c2 | ||
|
|
b833113ed6 | ||
|
|
1cf0e007d9 | ||
|
|
f74fff8356 | ||
|
|
ec23e3d0ca | ||
|
|
81a5466f50 | ||
|
|
bab47f098c | ||
|
|
1f600dff0d | ||
|
|
7b6603719e | ||
|
|
a219509bd7 | ||
|
|
f56102dd88 | ||
|
|
7a7b11bdeb | ||
|
|
f488212c3d | ||
|
|
13f20cbeb0 | ||
|
|
975103fc9d | ||
|
|
2fd4a47b33 | ||
|
|
2defb7ec35 | ||
|
|
df2de7ad66 | ||
|
|
ba19d4e6b1 | ||
|
|
897f22ee6e | ||
|
|
0f834a714f | ||
|
|
99ff07507e | ||
|
|
7c00f2d903 | ||
|
|
6291abf83e | ||
|
|
0259f636b0 | ||
|
|
875dae0755 | ||
|
|
fba1c59f84 | ||
|
|
86550d1563 | ||
|
|
2e44775170 | ||
|
|
4cfdc03efe | ||
|
|
b6aca7fae6 | ||
|
|
f0ae4e7e6c | ||
|
|
026adcd611 | ||
|
|
cff628e6a3 | ||
|
|
ed95646bec | ||
|
|
1edc5fa4f7 | ||
|
|
74f830486e | ||
|
|
a51f86ae28 | ||
|
|
02eb5ad4e8 | ||
|
|
aa357af4c5 | ||
|
|
da451ed369 | ||
|
|
7f63a1e82d | ||
|
|
a3585a859f | ||
|
|
4473043623 | ||
|
|
8980fc9cb1 | ||
|
|
73ddc6c4b9 | ||
|
|
23bbd10b9a | ||
|
|
e623351d24 | ||
|
|
ab816e9e09 | ||
|
|
8ca651b2fa | ||
|
|
261d9ba59c | ||
|
|
55bf83e02a | ||
|
|
69ae65b4bb | ||
|
|
fff3e5c9ce | ||
|
|
f05bb205e5 | ||
|
|
97cae30a51 | ||
|
|
54107ca937 | ||
|
|
ec24392664 | ||
|
|
8ea778a8d4 | ||
|
|
b28d818c86 | ||
|
|
a0d62079eb | ||
|
|
0b86c55d00 | ||
|
|
690656e4d1 | ||
|
|
1c542b33e7 | ||
|
|
2651b6fd93 | ||
|
|
456f304338 | ||
|
|
976b1d54bd | ||
|
|
c2ef5abd79 | ||
|
|
afe4da1f65 | ||
|
|
a8f4bab23e | ||
|
|
1fee4fc940 | ||
|
|
0fce269a82 | ||
|
|
55faeaa1d1 | ||
|
|
918af4ecf5 | ||
|
|
0200bf1674 | ||
|
|
cfbc04d3a4 | ||
|
|
b047f6ca9a | ||
|
|
c64ec1b2d8 | ||
|
|
88f7b3d345 | ||
|
|
ba8188978d | ||
|
|
03dabc1537 | ||
|
|
be777c1784 | ||
|
|
c023b32e75 | ||
|
|
ceaab4eeb7 | ||
|
|
4301d92d0b | ||
|
|
218b512526 | ||
|
|
118558cdfd | ||
|
|
0864d306d6 | ||
|
|
d1d820da73 | ||
|
|
1a4947a529 | ||
|
|
10dae4fa11 | ||
|
|
deeb86bd5f | ||
|
|
dcb8110c2f | ||
|
|
b9e1601aa4 | ||
|
|
9f8fdb2dfb | ||
|
|
b2804a2f56 | ||
|
|
c6652ba9b4 | ||
|
|
0953d0d76e | ||
|
|
867f8ff199 | ||
|
|
6a3ebb9e5d | ||
|
|
52c95d4d0c | ||
|
|
021ce48351 | ||
|
|
fe9ba68f41 | ||
|
|
6752f1cfef | ||
|
|
705000b46a | ||
|
|
4277ff95da | ||
|
|
5c43378d71 | ||
|
|
33b228a86f | ||
|
|
8054e48d4a | ||
|
|
66479e83fc | ||
|
|
1dfb4c7d29 | ||
|
|
df34c4a0ae | ||
|
|
0bbf8f4914 | ||
|
|
64c716ea07 | ||
|
|
36c54521d7 | ||
|
|
2aad2efc3c | ||
|
|
d69e45f7bf | ||
|
|
e35fbea415 | ||
|
|
9ed52fd070 | ||
|
|
a9dad5c79b | ||
|
|
6ee514910f | ||
|
|
61b1c0b1e6 | ||
|
|
2c30189fea | ||
|
|
6179c4c8ec | ||
|
|
39faff29e1 | ||
|
|
916d7e6d88 | ||
|
|
f2ef5f2df8 | ||
|
|
9b8778f823 | ||
|
|
6cc3745c5f | ||
|
|
1d982f3306 | ||
|
|
b01b18647a | ||
|
|
a566f6ed5a | ||
|
|
3cef1b5505 | ||
|
|
d18f93d87f | ||
|
|
aa90583116 | ||
|
|
7dc4b283b3 | ||
|
|
a3a4a057e5 | ||
|
|
d1f71508fe | ||
|
|
ef8b1f40e4 | ||
|
|
e5e4675c1b | ||
|
|
0c87bbac21 | ||
|
|
44ac2626ec | ||
|
|
47a861a243 | ||
|
|
7ec3d70c60 | ||
|
|
d1422826b8 | ||
|
|
ee5b30884f | ||
|
|
d8b1e3432a | ||
|
|
7df534b4b5 | ||
|
|
2a92122dad | ||
|
|
18096cb6ee | ||
|
|
9dcdd7e41c | ||
|
|
f451af2188 | ||
|
|
0c90875b61 | ||
|
|
56d64b325c | ||
|
|
f1241cf803 | ||
|
|
12c2b10419 | ||
|
|
af86cabcff | ||
|
|
c6176438ee | ||
|
|
8cdce0c302 | ||
|
|
2627a16c36 | ||
|
|
c9ad0ef170 | ||
|
|
497193750b | ||
|
|
02623ab827 | ||
|
|
5cbaf80f75 | ||
|
|
ada4f01ee2 | ||
|
|
19d7082d58 | ||
|
|
5a9540b9c3 | ||
|
|
fe20741409 | ||
|
|
bfe80dcacb | ||
|
|
1a5b8819fa | ||
|
|
3553bfa2d9 | ||
|
|
732b6eb641 | ||
|
|
d9bc435e2c | ||
|
|
ab0b2b152c | ||
|
|
afd65c809d | ||
|
|
bae103257c | ||
|
|
8d192445a7 | ||
|
|
1a130d0be3 | ||
|
|
17fa031064 | ||
|
|
a146d73f4b | ||
|
|
f6f6a80639 | ||
|
|
9bf8fa5b63 | ||
|
|
d6444c77e4 | ||
|
|
b4fa86b8f3 | ||
|
|
2ab9599518 | ||
|
|
aa5d3fc89d | ||
|
|
15dd6f5a5c | ||
|
|
232ad31553 | ||
|
|
e97984d6c5 | ||
|
|
a02f12c6ce | ||
|
|
d7965d1193 | ||
|
|
03543d5cfc | ||
|
|
5340ca84e5 | ||
|
|
3d5e094fd8 | ||
|
|
d1d86b04fb | ||
|
|
9a9bee7154 | ||
|
|
eb169f73fa | ||
|
|
a3a20f8820 | ||
|
|
d38fcae562 | ||
|
|
d810a629c8 | ||
|
|
532548572d | ||
|
|
3808852894 | ||
|
|
a80161a316 | ||
|
|
ea4f678d05 | ||
|
|
3f3a3f9147 | ||
|
|
cad5fb6067 | ||
|
|
4692dde3d5 | ||
|
|
aff2d4dd76 | ||
|
|
f41559ae2f | ||
|
|
e5113ef20a | ||
|
|
e0e69b3d3d | ||
|
|
e2d29af045 | ||
|
|
291ebc5f0e | ||
|
|
58cd2fcacb | ||
|
|
61908dd0c0 | ||
|
|
21bbbf312f | ||
|
|
d91fbaa2c0 | ||
|
|
8e0c710815 | ||
|
|
edb7b38e0c | ||
|
|
a18cc9a486 | ||
|
|
88222034e6 | ||
|
|
f3009ee781 | ||
|
|
d2f8f507a1 | ||
|
|
629f75d4e8 | ||
|
|
246d73c070 | ||
|
|
04083f478a | ||
|
|
c99503980f | ||
|
|
04bf9cd185 | ||
|
|
a400d95444 | ||
|
|
2330aa2bc9 | ||
|
|
e6a711693b | ||
|
|
977868c1fa | ||
|
|
7a85a75ca9 | ||
|
|
2458a82031 | ||
|
|
968f1e421f | ||
|
|
a28a9f732d | ||
|
|
a7d3a2e221 | ||
|
|
639098f401 | ||
|
|
aff9ba29e7 | ||
|
|
db95565b70 | ||
|
|
b5920cbc32 | ||
|
|
eb0cd796b0 | ||
|
|
9559986499 | ||
|
|
9bd3049ba0 | ||
|
|
18dc2889ce | ||
|
|
983bfbaa1b | ||
|
|
421efc62ad | ||
|
|
fd54864f4d | ||
|
|
3d4f804e5f | ||
|
|
20aecf9514 | ||
|
|
4349eda310 | ||
|
|
561445de03 | ||
|
|
2c4bd445a5 | ||
|
|
4daaa37ed9 | ||
|
|
31b12c3625 | ||
|
|
91b22864d7 | ||
|
|
1921e4c5fd | ||
|
|
9861d1bf78 | ||
|
|
7561ff4c9f | ||
|
|
4a8ed7268e | ||
|
|
5b0aa3fc7d | ||
|
|
b237ef5c24 | ||
|
|
2837126d60 | ||
|
|
859a18470e | ||
|
|
08ded5bb71 | ||
|
|
052f34a9f5 | ||
|
|
c162a3acc6 | ||
|
|
e49cbbbbec | ||
|
|
35c906c777 | ||
|
|
c678031d00 | ||
|
|
07a3664de4 | ||
|
|
b025bed83c | ||
|
|
5e2b7c87d0 | ||
|
|
513b2fcae8 | ||
|
|
6c4950615d | ||
|
|
cf71c1a18f | ||
|
|
a5caae8991 | ||
|
|
b77ffc1786 | ||
|
|
e3cf5369d8 | ||
|
|
f46e996570 | ||
|
|
7698625213 | ||
|
|
a2aefbf928 | ||
|
|
0249e959ab | ||
|
|
235bec0ee6 | ||
|
|
62d13a12d1 | ||
|
|
1905a2c96c | ||
|
|
d42cecb4df | ||
|
|
4238834cb5 | ||
|
|
f7555abed7 | ||
|
|
efc3327cec | ||
|
|
05f6b77520 | ||
|
|
d73a179f59 | ||
|
|
58bcbc3aad | ||
|
|
06667c1b36 | ||
|
|
60d1dea537 | ||
|
|
3ba51f04b5 | ||
|
|
846d21ed13 | ||
|
|
35f2c38788 | ||
|
|
955e5066f3 | ||
|
|
30cc1611f8 | ||
|
|
1ded94bd2d | ||
|
|
2a4fbea0a8 | ||
|
|
d8b2253351 | ||
|
|
f69a4c3997 | ||
|
|
ac7eaf3cc1 | ||
|
|
d6de75e3b7 | ||
|
|
a725f20034 | ||
|
|
6800c76e40 | ||
|
|
28d4a018fd | ||
|
|
5609a39dd4 | ||
|
|
ab2a81cd9b | ||
|
|
a8eee1dbcf | ||
|
|
256541a05a | ||
|
|
5028a5de8c | ||
|
|
8cf8502485 | ||
|
|
5a47893cd2 | ||
|
|
38836da9af | ||
|
|
61146e491b | ||
|
|
224b453430 | ||
|
|
c2d6ee90ac | ||
|
|
cc3486ab63 | ||
|
|
82afd528d6 | ||
|
|
03e8d38a1c | ||
|
|
0f02cc6d29 | ||
|
|
2b3a18c629 | ||
|
|
df80ff9533 | ||
|
|
78a3b1a5be | ||
|
|
42fe9ae1ba | ||
|
|
a60d027169 | ||
|
|
757d71cd40 | ||
|
|
1bae3370fa | ||
|
|
f4e5b8730a | ||
|
|
9e4edd8843 | ||
|
|
c0994b38e8 | ||
|
|
70e3e6423e | ||
|
|
a6d97a601c | ||
|
|
be2296185f | ||
|
|
888a2f8f95 | ||
|
|
855e2c61c5 | ||
|
|
7846fa7f93 | ||
|
|
869abb0469 | ||
|
|
a073e7f308 | ||
|
|
a43fadb4e8 | ||
|
|
118505f91e | ||
|
|
c5718057aa | ||
|
|
e8b73b4943 | ||
|
|
84c1fb5402 | ||
|
|
3fc781c1c1 | ||
|
|
5c99a14559 | ||
|
|
baff1308e7 | ||
|
|
d46537a24f | ||
|
|
5ea5f54097 | ||
|
|
899648d8e4 | ||
|
|
0ebfa6af23 | ||
|
|
1e61171f9c | ||
|
|
eb61563db4 | ||
|
|
f588c91570 | ||
|
|
1ef7ea3f27 | ||
|
|
1c267a0030 | ||
|
|
bea081322e | ||
|
|
75422c73e8 | ||
|
|
baabc939d8 | ||
|
|
98b49104cb | ||
|
|
5cc738e2ba | ||
|
|
fe9d0da14a | ||
|
|
30ece4b8c8 | ||
|
|
c39d89258a | ||
|
|
2ea3f10cb0 | ||
|
|
135406ebfe | ||
|
|
a71874a8c4 | ||
|
|
5f6ea3cf29 | ||
|
|
6cea49d316 | ||
|
|
5c93adde3b | ||
|
|
51411d300a | ||
|
|
48a954e077 | ||
|
|
b74f2ecad5 | ||
|
|
0a4fa7fb15 | ||
|
|
ca201c8dfe | ||
|
|
1cd7493bf3 | ||
|
|
4c752f9a15 | ||
|
|
8ed00452f9 | ||
|
|
e1426b484e | ||
|
|
3a1046f215 | ||
|
|
471628618c | ||
|
|
73896ae64b | ||
|
|
ce6810129a | ||
|
|
4def341b66 | ||
|
|
14fdd4aa32 | ||
|
|
010f38ea25 | ||
|
|
76c41d5156 | ||
|
|
05f02c2197 | ||
|
|
656c5f2a3e | ||
|
|
01fe226a92 | ||
|
|
fe3c2feccb | ||
|
|
3508adb5ef | ||
|
|
3ab1725bb6 | ||
|
|
a57d4dc4b1 | ||
|
|
a1fd8391da | ||
|
|
8f59cdea52 | ||
|
|
def180e785 | ||
|
|
9becf699bd | ||
|
|
9862fd5d1a | ||
|
|
562e1e5e68 | ||
|
|
b00cb7e613 | ||
|
|
ad058674f6 | ||
|
|
6d3de4d691 | ||
|
|
7c8039d91d | ||
|
|
199aea0636 | ||
|
|
e3b404c983 | ||
|
|
0ff571db14 | ||
|
|
f12019d77a | ||
|
|
5f94e882a9 | ||
|
|
bbbe69a9df | ||
|
|
c8de8bc225 | ||
|
|
90b3a2155d | ||
|
|
01e5916bf3 | ||
|
|
33f0e6febf | ||
|
|
aa67e66834 | ||
|
|
f2aedd42b7 | ||
|
|
d93a31dda0 | ||
|
|
99360626e0 | ||
|
|
483bab095b | ||
|
|
ea5224c7e3 | ||
|
|
b9101d9b1c | ||
|
|
9789a2509f | ||
|
|
86ac71ea2e | ||
|
|
ddb95b925d | ||
|
|
723ebc4055 | ||
|
|
882c0bde42 | ||
|
|
e6ea47e75f | ||
|
|
663fb1485a | ||
|
|
593d3af6d4 | ||
|
|
c1bed26fd1 | ||
|
|
cbdb22f1ad | ||
|
|
f4bbd02e69 | ||
|
|
e5f1c41df0 | ||
|
|
6fe8ce7ec4 | ||
|
|
894ac99687 | ||
|
|
a400871548 | ||
|
|
b78e2b6614 | ||
|
|
3630d2cfc7 | ||
|
|
25a7af0de6 | ||
|
|
2b22c06b34 | ||
|
|
49d006b9b1 | ||
|
|
5757c06f77 | ||
|
|
05cd767761 | ||
|
|
41dac08dd1 | ||
|
|
02e3ce779c | ||
|
|
0672a73c0b | ||
|
|
68a1fe64bb | ||
|
|
b267458f7e | ||
|
|
824311937a | ||
|
|
c6099f9863 | ||
|
|
7f8ed04fb4 | ||
|
|
305cade97b | ||
|
|
5c9b7d7000 | ||
|
|
f7ba43437e | ||
|
|
e590576247 | ||
|
|
143cef5ffb | ||
|
|
4b50ff9f59 | ||
|
|
c9e7087548 | ||
|
|
98fce17124 | ||
|
|
0ec245dce1 | ||
|
|
aa3eb6b991 | ||
|
|
35e6ab2cce | ||
|
|
68d3ccd469 | ||
|
|
313e7ce014 | ||
|
|
426a0f8c3b | ||
|
|
6518ded13f | ||
|
|
7e30cb95ec | ||
|
|
b926a26dc8 | ||
|
|
e205a0d3e0 | ||
|
|
17de4268b0 | ||
|
|
e4bc027aef | ||
|
|
304c55f038 | ||
|
|
5f788dfda7 | ||
|
|
be35be8908 | ||
|
|
733a96a987 | ||
|
|
aa69772127 | ||
|
|
bf276510ba | ||
|
|
2e1ae4c7d5 | ||
|
|
06c0666b93 | ||
|
|
fb95174f67 | ||
|
|
944b4ae856 | ||
|
|
f671adceb3 | ||
|
|
bf5f478dcc | ||
|
|
ffafc2313d | ||
|
|
acac16cef0 | ||
|
|
f2ccee1c6b | ||
|
|
dfed4f2c1f | ||
|
|
310a88b0a5 | ||
|
|
fe60acab63 | ||
|
|
e601604362 | ||
|
|
d997a2b679 | ||
|
|
9383277e64 | ||
|
|
c7e51f5b2c | ||
|
|
fcb50dfcda | ||
|
|
f7a2389523 | ||
|
|
435b590b3e | ||
|
|
bde9835d5d | ||
|
|
08173c7663 | ||
|
|
6e687aeb1e | ||
|
|
a06b59784a | ||
|
|
80eed213d9 | ||
|
|
9f2c06bcd3 | ||
|
|
1dccfdba3b | ||
|
|
3522f76d37 | ||
|
|
53894ac4ea | ||
|
|
676b3677b9 | ||
|
|
c78298d1ed | ||
|
|
8b3cfc7a67 | ||
|
|
375f715b78 | ||
|
|
fded75e3f4 | ||
|
|
81fbb80788 | ||
|
|
071b2c1153 | ||
|
|
671062af31 | ||
|
|
7d845569d4 | ||
|
|
dbeaf87f8f | ||
|
|
9413e491d4 | ||
|
|
bb06ee8773 | ||
|
|
c46ffad7d3 | ||
|
|
86e9239a99 | ||
|
|
3ed1872c44 | ||
|
|
a7a56d939a | ||
|
|
ea6744872e | ||
|
|
73d1c57e53 | ||
|
|
af3d0dcb00 | ||
|
|
2b97db659a | ||
|
|
03b1a5521f | ||
|
|
b17a405a81 | ||
|
|
56de3da3a8 | ||
|
|
c682e49090 | ||
|
|
fd28741cb8 | ||
|
|
0256f05e7d | ||
|
|
0ec49d0802 | ||
|
|
bf85675e42 | ||
|
|
d2e8a2dba4 | ||
|
|
5135aa64c9 | ||
|
|
260ef6dc9d | ||
|
|
eff357dc8e | ||
|
|
ef03ba4f60 | ||
|
|
f39b1bb14d | ||
|
|
3d0421762a | ||
|
|
fb6b84d920 | ||
|
|
e80b1825eb | ||
|
|
710659560b | ||
|
|
81392ee8a0 | ||
|
|
e53e21857e | ||
|
|
334d3918c5 | ||
|
|
a7b03ef6f6 | ||
|
|
5701609857 | ||
|
|
0a49027840 | ||
|
|
7bbc53c8af | ||
|
|
865f62dbb8 | ||
|
|
20e391cf1c | ||
|
|
9a63071292 | ||
|
|
05aae219dd | ||
|
|
6c0f921318 | ||
|
|
9ebffecd47 | ||
|
|
4a5f3b901d | ||
|
|
41e794f684 | ||
|
|
afeeceaf77 | ||
|
|
59857e66e7 | ||
|
|
c5cf9fc71d | ||
|
|
d93e27a40d | ||
|
|
5fb78bcd80 | ||
|
|
7a2bec0a6b | ||
|
|
b4633bdfe6 | ||
|
|
1d8898e974 | ||
|
|
ec842d21a2 | ||
|
|
4d898bfef0 | ||
|
|
95b7dee402 | ||
|
|
dcbafb70eb | ||
|
|
852c122e56 | ||
|
|
6a90c62456 | ||
|
|
45d9657455 | ||
|
|
f1ecbeb9f6 | ||
|
|
ff394356b6 | ||
|
|
873c9ebd8c | ||
|
|
8c8c414bda | ||
|
|
093781727a | ||
|
|
ff35397292 | ||
|
|
48974d1b5a | ||
|
|
5b3dfc26de | ||
|
|
d31244e747 | ||
|
|
722e345cbe | ||
|
|
893364d52d | ||
|
|
cf3801cba0 | ||
|
|
7585ac450e | ||
|
|
2af5257e10 | ||
|
|
98a571c7d2 | ||
|
|
485acf42d0 | ||
|
|
d3a3007bef | ||
|
|
0cea3312d6 | ||
|
|
33623db6ea | ||
|
|
e596d72cf4 | ||
|
|
a968a5531c | ||
|
|
bbadd179fa | ||
|
|
c450eb849f | ||
|
|
99ffb986f1 | ||
|
|
049350b112 | ||
|
|
b074a9eaf3 | ||
|
|
2fae820ee4 | ||
|
|
b138c1b4a0 | ||
|
|
c121a233d6 | ||
|
|
333cb37ed3 | ||
|
|
70d6d398be | ||
|
|
22472ea1f1 | ||
|
|
e452c3516a | ||
|
|
91327a3c6c | ||
|
|
1833388ea4 | ||
|
|
13f270f426 | ||
|
|
40a0391092 | ||
|
|
80e816927f | ||
|
|
6d6ab73b72 | ||
|
|
a9b9b7df03 | ||
|
|
d08998eb1c | ||
|
|
050815dd7e | ||
|
|
443c3c199e | ||
|
|
936e190908 | ||
|
|
a00bf15a44 | ||
|
|
f47eb61aee | ||
|
|
ff9a89380c | ||
|
|
37bb9a9a97 | ||
|
|
e4bd24b4e8 | ||
|
|
63de537f8f | ||
|
|
7468268e1a | ||
|
|
57673384e9 | ||
|
|
b3c1723c5c | ||
|
|
f453abf149 | ||
|
|
c01c02d723 | ||
|
|
ac9f316100 | ||
|
|
c76823dc5f | ||
|
|
742aa0e473 | ||
|
|
219589b8ea | ||
|
|
0a9b2ae966 | ||
|
|
d3fc2a0164 | ||
|
|
8761b860fd | ||
|
|
668a5f1fa5 | ||
|
|
40adf992ed | ||
|
|
58f5e90f8f | ||
|
|
42813b90dd | ||
|
|
9bae77bfa1 | ||
|
|
ea25d9bce4 | ||
|
|
088d6cab55 | ||
|
|
d6dc4d291c | ||
|
|
ec6a79d9d6 | ||
|
|
1934360cf4 | ||
|
|
d6b1bcc375 | ||
|
|
4b02c19aa3 | ||
|
|
c510959b76 | ||
|
|
d4069fd835 | ||
|
|
c725f9eebf | ||
|
|
63e15ff686 | ||
|
|
0c1ac011f2 | ||
|
|
b2dd5b7cad | ||
|
|
2da1590cd9 | ||
|
|
a096f963ef | ||
|
|
cd734c7d5d | ||
|
|
0106751e64 | ||
|
|
6f2f5bce86 | ||
|
|
e0498895bc | ||
|
|
accf860796 | ||
|
|
f4cd25f8ad | ||
|
|
e90e8a55ce | ||
|
|
33bc780c1f | ||
|
|
0b91ef04a0 | ||
|
|
e2338b0ab1 | ||
|
|
6d8408b0ad | ||
|
|
3e494f6519 | ||
|
|
c0f494c758 | ||
|
|
04f4538700 | ||
|
|
ffcd9990eb | ||
|
|
4b8e77be5c | ||
|
|
cb3b9d6921 | ||
|
|
361b1e7991 | ||
|
|
4dd58f7c36 | ||
|
|
b7f17ef196 | ||
|
|
30116c183f | ||
|
|
60987f4623 | ||
|
|
874f2592ca | ||
|
|
c82d0a915a | ||
|
|
f44507a69b | ||
|
|
2a3e22344e | ||
|
|
398c77eb35 | ||
|
|
3251611c6d | ||
|
|
c5fc3d24a9 | ||
|
|
ee135f2032 | ||
|
|
a503329b77 | ||
|
|
54f48fbe4d | ||
|
|
4f2f2dd485 | ||
|
|
a0deddd4e2 | ||
|
|
90d835059a | ||
|
|
fe0f6af9f4 | ||
|
|
7f29f362f5 | ||
|
|
ca14a1e8d0 | ||
|
|
f0b0d21386 | ||
|
|
28b32edfab | ||
|
|
f50f2c5ea0 | ||
|
|
a67fb22bee | ||
|
|
fafe56ecb4 | ||
|
|
06e2dc722d | ||
|
|
9f8854ae80 | ||
|
|
5bb190d974 | ||
|
|
bb7d8e85a0 | ||
|
|
eb98c0713a | ||
|
|
121f6f0bf8 | ||
|
|
75766573e9 | ||
|
|
059b9eb322 | ||
|
|
1d6dad8f4f | ||
|
|
765150c19a | ||
|
|
6215af5d74 | ||
|
|
c605ff1767 | ||
|
|
e07cf8d11f | ||
|
|
8701174157 | ||
|
|
df766e56da | ||
|
|
b6495f3710 | ||
|
|
096faae844 | ||
|
|
4250dcf3f0 | ||
|
|
0f14e235b4 | ||
|
|
62bdf41882 | ||
|
|
a1ceb8b314 | ||
|
|
4f3a27cf54 | ||
|
|
fd90949e15 | ||
|
|
30a6ee5d3d | ||
|
|
3de19b22b6 | ||
|
|
f4d98130a5 | ||
|
|
fc60dc94fc | ||
|
|
2e8497dc01 | ||
|
|
eeb289ea44 | ||
|
|
e26f00e268 | ||
|
|
942cfbbf53 | ||
|
|
e4b70c2af1 | ||
|
|
62b6a1513c | ||
|
|
3f801cb762 | ||
|
|
ed959459ef | ||
|
|
92079fd6f7 | ||
|
|
376490725c | ||
|
|
f11f16c23b | ||
|
|
97bc3ef585 | ||
|
|
92cec80b73 | ||
|
|
6d6690009c | ||
|
|
01692075c6 | ||
|
|
36feca6ce7 | ||
|
|
86374433b9 | ||
|
|
0bf891ba9d | ||
|
|
1d63683cf9 | ||
|
|
65a3b9e4c7 | ||
|
|
b82d26b71b | ||
|
|
169dbfecca | ||
|
|
47a6af52ba | ||
|
|
a9edbe9ecf | ||
|
|
93dcea6d35 | ||
|
|
f2054fde0f | ||
|
|
2edd55f01f | ||
|
|
06b5b83588 | ||
|
|
b5114fa73a | ||
|
|
962ddde1f3 | ||
|
|
e7c7b8ed53 | ||
|
|
5990addfef | ||
|
|
c270f0ad9a | ||
|
|
bb4ff4fdc7 | ||
|
|
661cd2aaf9 | ||
|
|
60b7f61e42 | ||
|
|
13980c4ee3 | ||
|
|
24b638cd82 | ||
|
|
18fbf332c2 | ||
|
|
d63b2000b6 | ||
|
|
f23dd5a431 | ||
|
|
e226a3d503 | ||
|
|
e324e2f6b6 | ||
|
|
f4bc796fb1 | ||
|
|
f4bed88e39 | ||
|
|
b2a492bdab | ||
|
|
5748dea4bc | ||
|
|
536a9d20fd | ||
|
|
7265cf03d9 | ||
|
|
507a50de78 | ||
|
|
ab95a6ccb3 | ||
|
|
19bbeb042a | ||
|
|
e59e6444d0 | ||
|
|
7838aee0f1 | ||
|
|
1b561ee10f | ||
|
|
b4fe7527a6 | ||
|
|
0a68512055 | ||
|
|
bb58189352 |
2406 changed files with 184700 additions and 82211 deletions
0
.docker/dev/nginx/ssl.sh
Normal file → Executable file
0
.docker/dev/nginx/ssl.sh
Normal file → Executable file
|
|
@ -1,48 +1,192 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM alpine:3.24.3 AS builder
|
||||||
|
RUN apk add --no-cache php85 php85-json php-phar php-zip
|
||||||
|
RUN apk add --no-cache npm
|
||||||
|
RUN npm install -g gulp yarn
|
||||||
|
WORKDIR /source
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn install
|
||||||
|
COPY . .
|
||||||
|
# Patch release.php with hotfix from: https://github.com/xgbstar1/snappymail-docker/blob/main/Dockerfile, so that release.php doesn't fail with error
|
||||||
|
RUN sed -i 's_^if.*rename.*snappymail.v.0.0.0.*$_if (!!system("mv snappymail/v/0.0.0 snappymail/v/{$package->version}")) {_' cli/release.php || true
|
||||||
|
RUN php release.php
|
||||||
|
RUN set -eux; \
|
||||||
|
VERSION=$( ls build/dist/releases/webmail ); \
|
||||||
|
ls -al build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz; \
|
||||||
|
mkdir -p /snappymail; \
|
||||||
|
tar -zxvf build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz -C /snappymail; \
|
||||||
|
find /snappymail -type d -exec chmod 550 {} \; ; \
|
||||||
|
find /snappymail -type f -exec chmod 440 {} \; ; \
|
||||||
|
find /snappymail/data -type d -exec chmod 750 {} \; ; \
|
||||||
|
# Remove unneeded files
|
||||||
|
rm -v /snappymail/README.md /snappymail/_include.php
|
||||||
|
|
||||||
# Inspired by the original Rainloop dockerfile from youtous on GitLab
|
# Inspired by the original Rainloop dockerfile from youtous on GitLab
|
||||||
FROM php:7.4-fpm-buster
|
FROM php:8.5-fpm-alpine AS final
|
||||||
|
|
||||||
ARG FILES_ZIP
|
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
|
||||||
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm based on Debian Buster"
|
|
||||||
|
|
||||||
ENV UID=991 GID=991 UPLOAD_MAX_SIZE=25M LOG_TO_STDERR=true MEMORY_LIMIT=128M SECURE_COOKIES=true
|
|
||||||
ENV fpm.pool.clear_env=false
|
|
||||||
|
|
||||||
# Install dependencies such as nginx
|
# Install dependencies such as nginx
|
||||||
RUN mkdir -p /usr/share/man/man1/ /usr/share/man/man3/ /usr/share/man/man7/ && \
|
RUN apk add --no-cache ca-certificates nginx supervisor bash
|
||||||
apt-get update -q --fix-missing && \
|
|
||||||
apt-get -y upgrade && \
|
|
||||||
apt-get install --no-install-recommends -y \
|
|
||||||
apt-transport-https gnupg openssl wget curl ca-certificates nginx supervisor sudo \
|
|
||||||
unzip libzip-dev libxml2-dev libldb-dev libldap2-dev \
|
|
||||||
sqlite3 libsqlite3-dev libsqlite3-0 libpq-dev postgresql-client mariadb-client logrotate \
|
|
||||||
zip mlocate libpcre3-dev libicu-dev \
|
|
||||||
build-essential chrpath libssl-dev \
|
|
||||||
libxft-dev libfreetype6 libfreetype6-dev \
|
|
||||||
libpng-dev libjpeg62-turbo-dev \
|
|
||||||
libfontconfig1 libfontconfig1-dev \
|
|
||||||
&& \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
RUN php -m && \
|
# apcu
|
||||||
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
RUN set -eux; \
|
||||||
docker-php-ext-configure intl && \
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
docker-php-ext-configure gd --with-freetype-dir=/usr/include --with-jpeg-dir=/usr/include/ && \
|
pecl install apcu; \
|
||||||
docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \
|
docker-php-ext-enable apcu; \
|
||||||
php -m
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies;
|
||||||
|
|
||||||
|
# gd
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache freetype libjpeg-turbo libpng; \
|
||||||
|
apk add --no-cache --virtual .deps freetype-dev libjpeg-turbo-dev libpng-dev; \
|
||||||
|
docker-php-ext-configure gd --with-freetype --with-jpeg; \
|
||||||
|
docker-php-ext-install gd; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# gmagick
|
||||||
|
# RUN set -eux; \
|
||||||
|
# apk add --no-cache graphicsmagick libgomp; \
|
||||||
|
# apk add --no-cache --virtual .deps graphicsmagick-dev libtool; \
|
||||||
|
# apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
# pecl install gmagick-2.0.6RC1; \
|
||||||
|
# docker-php-ext-enable gmagick; \
|
||||||
|
# docker-php-source delete; \
|
||||||
|
# apk del .build-dependencies; \
|
||||||
|
# apk del .deps
|
||||||
|
|
||||||
|
# gnupg
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache gnupg gpgme; \
|
||||||
|
apk add --no-cache --virtual .deps gpgme-dev; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
pecl install gnupg; \
|
||||||
|
docker-php-ext-enable gnupg; \
|
||||||
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# imagick
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache imagemagick libgomp; \
|
||||||
|
apk add --no-cache --virtual .deps imagemagick-dev; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
echo | pecl install imagick; \
|
||||||
|
docker-php-ext-enable imagick; \
|
||||||
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# intl
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache icu-libs; \
|
||||||
|
apk add --no-cache --virtual .deps icu-dev; \
|
||||||
|
docker-php-ext-configure intl; \
|
||||||
|
docker-php-ext-install intl; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# ldap
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache libldap; \
|
||||||
|
apk add --no-cache --virtual .deps openldap-dev; \
|
||||||
|
docker-php-ext-configure ldap; \
|
||||||
|
docker-php-ext-install ldap; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# mysql
|
||||||
|
RUN docker-php-ext-install pdo_mysql
|
||||||
|
|
||||||
|
# postgres
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache postgresql-libs; \
|
||||||
|
apk add --no-cache --virtual .deps postgresql-dev; \
|
||||||
|
docker-php-ext-install pdo_pgsql; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# redis
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache liblzf zstd-libs; \
|
||||||
|
apk add --no-cache --virtual .deps zstd-dev; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
pecl install igbinary; \
|
||||||
|
docker-php-ext-enable igbinary; \
|
||||||
|
pecl install --configureoptions 'enable-redis-igbinary="yes" enable-redis-lzf="yes" enable-redis-zstd="yes"' redis; \
|
||||||
|
docker-php-ext-enable redis; \
|
||||||
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# sqlite
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache sqlite-libs; \
|
||||||
|
apk add --no-cache --virtual .deps sqlite-dev; \
|
||||||
|
docker-php-ext-install pdo_sqlite; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# tidy
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache tidyhtml; \
|
||||||
|
apk add --no-cache --virtual .deps tidyhtml-dev; \
|
||||||
|
docker-php-ext-install tidy; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# uuid
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache libuuid; \
|
||||||
|
apk add --no-cache --virtual .deps util-linux-dev; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
pecl install uuid; \
|
||||||
|
docker-php-ext-enable uuid; \
|
||||||
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# xxtea - Manually install php8 compatible version from https://github.com/xxtea/xxtea-pecl master branch
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
wget -q https://github.com/xxtea/xxtea-pecl/tarball/3f5888a29045e12301254151737c5dab4523a1c1 -O xxtea.tar; \
|
||||||
|
echo '9cbfd9c27255767deb26ddedf69e738d401d88ac9762d82c8510f9768842ca18 xxtea.tar' | sha256sum -c -; \
|
||||||
|
tar -C /usr/src -xvf xxtea.tar; \
|
||||||
|
cd /usr/src/xxtea-xxtea-pecl-3f5888a; \
|
||||||
|
phpize; \
|
||||||
|
./configure --with-php-config=/usr/local/bin/php-config --enable-xxtea=yes; \
|
||||||
|
make install; \
|
||||||
|
docker-php-ext-enable xxtea; \
|
||||||
|
cd -; \
|
||||||
|
rm -fv xxtea.tar; \
|
||||||
|
rm -rfv /usr/src/xxtea*; \
|
||||||
|
apk del .build-dependencies;
|
||||||
|
|
||||||
|
# zip
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache libzip; \
|
||||||
|
apk add --no-cache --virtual .deps libzip-dev; \
|
||||||
|
docker-php-ext-install zip; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
# Install snappymail
|
# Install snappymail
|
||||||
WORKDIR /tmp
|
# The 'www-data' user/group in alpine is 82:82. The 'nginx' user/group in alpine is 101:101, and is part of www-data group
|
||||||
COPY ${FILES_ZIP} .
|
COPY --chown=www-data:www-data --from=builder /snappymail /snappymail
|
||||||
RUN mkdir /snappymail && \
|
# Use a custom snappymail data folder
|
||||||
unzip -q ${FILES_ZIP} -d /snappymail && \
|
RUN mv -v /snappymail/data /var/lib/snappymail;
|
||||||
find /snappymail -type d -exec chmod 755 {} \; && \
|
# Setup configs
|
||||||
find /snappymail -type f -exec chmod 644 {} \; && \
|
COPY --chown=root:root .docker/release/files /
|
||||||
rm -rf ${FILES_ZIP}
|
RUN set -eux; \
|
||||||
|
chown www-data:www-data /snappymail/include.php; \
|
||||||
|
chmod 440 /snappymail/include.php; \
|
||||||
|
chmod +x /entrypoint.sh; \
|
||||||
|
# Disable the built-in php-fpm configs, since we're using our own config
|
||||||
|
mv -v /usr/local/etc/php-fpm.d/docker.conf /usr/local/etc/php-fpm.d/docker.conf.disabled; \
|
||||||
|
mv -v /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.disabled; \
|
||||||
|
mv -v /usr/local/etc/php-fpm.d/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf.disabled;
|
||||||
|
|
||||||
# Install other content
|
USER root
|
||||||
COPY files /
|
WORKDIR /snappymail
|
||||||
RUN chmod +x /entrypoint.sh && chmod +x /logrotate-loop.sh
|
VOLUME /var/lib/snappymail
|
||||||
VOLUME /snappymail/data
|
|
||||||
EXPOSE 8888
|
EXPOSE 8888
|
||||||
|
EXPOSE 9000
|
||||||
|
ENTRYPOINT []
|
||||||
CMD ["/entrypoint.sh"]
|
CMD ["/entrypoint.sh"]
|
||||||
|
|
|
||||||
96
.docker/release/files/entrypoint.sh
Normal file → Executable file
96
.docker/release/files/entrypoint.sh
Normal file → Executable file
|
|
@ -1,23 +1,26 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
# Create not root user
|
DEBUG=${DEBUG:-}
|
||||||
groupadd --gid "$GID" php-cli -f
|
if [ "$DEBUG" = 'true' ]; then
|
||||||
adduser --uid "$UID" --disabled-password --gid "$GID" --shell /bin/bash --home /home/php-cli php-cli --force --gecos ""
|
set -x
|
||||||
|
fi
|
||||||
|
UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
|
||||||
|
MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
|
||||||
|
SECURE_COOKIES=${SECURE_COOKIES:-true}
|
||||||
|
|
||||||
|
# Disable listening on ipv6 for ipv4-only environments, or nginx will fail to start
|
||||||
|
if ! ip a | grep 'inet6 ' > /dev/null; then
|
||||||
|
echo "[INFO] ipv6 is disabled. Configuring nginx to not listen on ipv6"
|
||||||
|
sed -i '/listen \[::\]:/d' /etc/nginx/nginx.conf
|
||||||
|
fi
|
||||||
|
|
||||||
# Set attachment size limit
|
# Set attachment size limit
|
||||||
sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
|
sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
|
||||||
sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf
|
sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf
|
||||||
|
|
||||||
# Set log output to STDERR if wanted (LOG_TO_STDERR=true)
|
|
||||||
if [ "$LOG_TO_STDERR" = true ]; then
|
|
||||||
echo "[INFO] Logging to stderr activated"
|
|
||||||
sed -i "s/.*error_log.*$/error_log \/dev\/stderr warn;/" /etc/nginx/nginx.conf
|
|
||||||
sed -i "s/.*error_log.*$/php_admin_value[error_log] = \/dev\/stderr/" /usr/local/etc/php-fpm.d/php-fpm.conf
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Secure cookies
|
# Secure cookies
|
||||||
if [ "${SECURE_COOKIES}" = true ]; then
|
if [ "${SECURE_COOKIES}" = 'true' ]; then
|
||||||
echo "[INFO] Secure cookies activated"
|
echo "[INFO] Secure cookies activated"
|
||||||
{
|
{
|
||||||
echo 'session.cookie_httponly = On';
|
echo 'session.cookie_httponly = On';
|
||||||
|
|
@ -26,43 +29,58 @@ if [ "${SECURE_COOKIES}" = true ]; then
|
||||||
} > /usr/local/etc/php/conf.d/cookies.ini;
|
} > /usr/local/etc/php/conf.d/cookies.ini;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy snappymail default config if absent
|
echo "[INFO] Snappymail version: $( ls /snappymail/snappymail/v )"
|
||||||
SNAPPYMAIL_CONFIG_FILE=/snappymail/data/_data_/_default_/configs/application.ini
|
|
||||||
|
# Set permissions on snappymail data
|
||||||
|
echo "[INFO] Setting permissions on /var/lib/snappymail"
|
||||||
|
chown -R www-data:www-data /var/lib/snappymail/
|
||||||
|
chmod 550 /var/lib/snappymail/
|
||||||
|
find /var/lib/snappymail/ -type d -exec chmod 750 {} \;
|
||||||
|
|
||||||
|
# Create snappymail default config if absent
|
||||||
|
SNAPPYMAIL_CONFIG_FILE=/var/lib/snappymail/_data_/_default_/configs/application.ini
|
||||||
if [ ! -f "$SNAPPYMAIL_CONFIG_FILE" ]; then
|
if [ ! -f "$SNAPPYMAIL_CONFIG_FILE" ]; then
|
||||||
echo "[INFO] Creating default Snappymail configuration"
|
echo "[INFO] Creating default Snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
|
||||||
mkdir -p $(dirname $SNAPPYMAIL_CONFIG_FILE)
|
# Run snappymail and exit. This populates the snappymail data directory and generates the config file
|
||||||
cp /usr/local/include/application.ini $SNAPPYMAIL_CONFIG_FILE
|
# On error, print php exception and exit
|
||||||
|
EXITCODE=
|
||||||
|
su - www-data -s /bin/sh -c 'php /snappymail/index.php' > /tmp/out || EXITCODE=$?
|
||||||
|
if [ -n "$EXITCODE" ]; then
|
||||||
|
cat /tmp/out
|
||||||
|
exit "$EXITCODE"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] Overriding values in snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
|
||||||
# Enable output of snappymail logs
|
# Enable output of snappymail logs
|
||||||
if [ "${LOG_TO_STDERR}" = true ]; then
|
sed '/^\; Enable logging/{
|
||||||
sed -z 's/\; Enable logging\nenable = Off/\; Enable logging\nenable = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
N
|
||||||
sed 's/^filename = .*/filename = "errors.log"/' -i $SNAPPYMAIL_CONFIG_FILE
|
s/enable = Off/enable = On/
|
||||||
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
}' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
# Redirect snappymail logs to stderr /stdout
|
||||||
else
|
sed 's/^filename = .*/filename = "stderr"/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed -z 's/\; Enable logging\nenable = On/\; Enable logging\nenable = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
fi
|
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
# Always enable snappymail Auth logging
|
# Always enable snappymail Auth logging
|
||||||
sed 's/^auth_logging = .*/auth_logging = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^auth_logging = .*/auth_logging = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed 's/^auth_logging_filename = .*/auth_logging_filename = "auth.log"/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^auth_logging_filename = .*/auth_logging_filename = "auth.log"/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed 's/^auth_logging_format = .*/auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^auth_logging_format = .*/auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
# Redirect snappymail logs to stderr /stdout
|
sed 's/^auth_syslog = .*/auth_syslog = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
mkdir -p /snappymail/data/_data_/_default_/logs/
|
|
||||||
# empty logs
|
|
||||||
cp /dev/null /snappymail/data/_data_/_default_/logs/errors.log
|
|
||||||
cp /dev/null /snappymail/data/_data_/_default_/logs/auth.log
|
|
||||||
chown -R php-cli:php-cli /snappymail/data/
|
|
||||||
|
|
||||||
# Fix permissions
|
(
|
||||||
chown -R $UID:$GID /snappymail/data /var/log /var/lib/nginx
|
while ! nc -vz -w 1 127.0.0.1 8888 > /dev/null 2>&1; do echo "[INFO] Checking whether nginx is alive"; sleep 1; done
|
||||||
chmod o+w /dev/stdout
|
while ! nc -vz -w 1 127.0.0.1 9000 > /dev/null 2>&1; do echo "[INFO] Checking whether php-fpm is alive"; sleep 1; done
|
||||||
chmod o+w /dev/stderr
|
# Create snappymail admin password if absent
|
||||||
|
SNAPPYMAIL_ADMIN_PASSWORD_FILE=/var/lib/snappymail/_data_/_default_/admin_password.txt
|
||||||
|
if [ ! -f "$SNAPPYMAIL_ADMIN_PASSWORD_FILE" ]; then
|
||||||
|
echo "[INFO] Creating Snappymail admin password file: $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
|
||||||
|
wget -T 1 -qO- 'http://127.0.0.1:8888/?/AdminAppData/0/12345/' > /dev/null
|
||||||
|
echo "[INFO] Snappymail Admin Panel ready at http://localhost:8888/?admin. Login using password in $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget -T 1 -qO- 'http://127.0.0.1:8888/' > /dev/null
|
||||||
# Touch supervisord PID file in order to fix permissions
|
echo "[INFO] Snappymail ready at http://localhost:8888/"
|
||||||
touch /run/supervisord.pid
|
) &
|
||||||
chown php-cli:php-cli /run/supervisord.pid
|
|
||||||
|
|
||||||
# RUN !
|
# RUN !
|
||||||
exec sudo -u php-cli -g php-cli /usr/bin/supervisord -c '/supervisor.conf' --pidfile '/run/supervisord.pid'
|
exec /usr/bin/supervisord -c /supervisor.conf --pidfile /run/supervisord.pid
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
/snappymail/data/_data_/_default_/logs/* {
|
|
||||||
size 10M
|
|
||||||
rotate 0
|
|
||||||
missingok
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,7 @@ http {
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
access_log off;
|
access_log off;
|
||||||
error_log /tmp/ngx_error.log error;
|
error_log /dev/stderr error;
|
||||||
|
|
||||||
sendfile on;
|
sendfile on;
|
||||||
keepalive_timeout 15;
|
keepalive_timeout 15;
|
||||||
|
|
@ -52,6 +52,8 @@ http {
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 8888;
|
listen 8888;
|
||||||
|
listen [::]:8888;
|
||||||
|
#listen [::]:8888 ipv6only=off;
|
||||||
root /snappymail;
|
root /snappymail;
|
||||||
index index.php;
|
index index.php;
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
|
|
@ -95,7 +97,7 @@ http {
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
fastcgi_param HTTP_PROXY "";
|
fastcgi_param HTTP_PROXY "";
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
fastcgi_pass unix:/tmp/php-fpm.sock;
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
fastcgi_intercept_errors on;
|
fastcgi_intercept_errors on;
|
||||||
fastcgi_request_buffering off;
|
fastcgi_request_buffering off;
|
||||||
fastcgi_param REMOTE_ADDR $http_x_real_ip;
|
fastcgi_param REMOTE_ADDR $http_x_real_ip;
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Simple script to invoke logrotate at regular intervals
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
# from https://github.com/misho-kr/docker-appliances/blob/master/nginx-nodejs/logrotate-loop.sh
|
|
||||||
|
|
||||||
LOGROTATE_BIN="logrotate"
|
|
||||||
|
|
||||||
STATE="$HOME/logrotate.state"
|
|
||||||
CONF="/etc/logrotate.d/snappymail"
|
|
||||||
|
|
||||||
export LOGROTATE_BIN STATE CONF
|
|
||||||
|
|
||||||
RUN_INTERVAL="3600" # every hour
|
|
||||||
|
|
||||||
# helper functions for logging
|
|
||||||
export FMT="%a %b %d %Y %H:%M:%S GMT%z (%Z)"
|
|
||||||
|
|
||||||
function log_date() {
|
|
||||||
echo "$(date +"$FMT"): $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Main loop of the logrotate service:
|
|
||||||
#
|
|
||||||
# while True:
|
|
||||||
# sleep N seconds
|
|
||||||
# run logrotate
|
|
||||||
#
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
function logrotate_loop() {
|
|
||||||
|
|
||||||
trap on_terminate TERM INT
|
|
||||||
|
|
||||||
local interval="${1}"
|
|
||||||
|
|
||||||
log_date "===================================================="
|
|
||||||
log_date
|
|
||||||
log_date "logrotate service starting (pid=$$)"
|
|
||||||
log_date "logrotate process will run every ${interval} seconds"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
|
|
||||||
current_time=$(date "+%s")
|
|
||||||
next_run_time=$(( current_time + interval ))
|
|
||||||
|
|
||||||
while (( current_time < next_run_time ))
|
|
||||||
do
|
|
||||||
logrotate_sleep $(( next_run_time - current_time ))
|
|
||||||
current_time=$(date "+%s")
|
|
||||||
done
|
|
||||||
|
|
||||||
logrotate_run
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# helper function to execute logrotate and pass it the right parameters
|
|
||||||
function logrotate_run() {
|
|
||||||
|
|
||||||
log_date "logrotate will run now"
|
|
||||||
${LOGROTATE_BIN} -s ${STATE} ${CONF}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Procedure to idle the execution for a number of seconds
|
|
||||||
#
|
|
||||||
# There are two requirements:
|
|
||||||
#
|
|
||||||
# - export the PID of the sleep command so that it can be terminated
|
|
||||||
# in case the logrotate service is being shutdown
|
|
||||||
# - keep this (bash) process responsive to SIGTERM while in sleep
|
|
||||||
# mode (normally the signal will be masked and will not be delivered
|
|
||||||
# until the subprocess completes)
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
proc_sleep_pid=""
|
|
||||||
|
|
||||||
function logrotate_sleep() {
|
|
||||||
|
|
||||||
local sleep_interval=${1}
|
|
||||||
|
|
||||||
log_date "logrotate will sleep for ${sleep_interval} seconds"
|
|
||||||
|
|
||||||
( exec -a "logrotate: sleep" sleep ${sleep_interval} )&
|
|
||||||
|
|
||||||
proc_sleep_pid=$!
|
|
||||||
wait ${proc_sleep_pid}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Signal handler for logrotate service to make sure the process exits:
|
|
||||||
#
|
|
||||||
# - properly by terminating the sleep process that is used to idle
|
|
||||||
# the service
|
|
||||||
# - gracefully by writing a message in the log
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
function on_terminate() {
|
|
||||||
|
|
||||||
log_date "logrotate will terminate"
|
|
||||||
log_date
|
|
||||||
|
|
||||||
kill -TERM ${proc_sleep_pid}
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# main
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
logrotate_loop ${1:-RUN_INTERVAL}
|
|
||||||
3
.docker/release/files/snappymail/include.php
Normal file
3
.docker/release/files/snappymail/include.php
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
define('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');
|
||||||
|
?>
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
[supervisord]
|
[supervisord]
|
||||||
nodaemon=true
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
|
||||||
[program:nginx]
|
[program:nginx]
|
||||||
command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
|
command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
user=php-cli
|
user=root
|
||||||
numprocs=1
|
numprocs=1
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=false
|
autorestart=false
|
||||||
|
|
@ -17,7 +20,7 @@ stderr_logfile_maxbytes=0
|
||||||
[program:php-fpm]
|
[program:php-fpm]
|
||||||
command=php-fpm -F
|
command=php-fpm -F
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
user=php-cli
|
user=root
|
||||||
numprocs=1
|
numprocs=1
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=false
|
autorestart=false
|
||||||
|
|
@ -27,34 +30,11 @@ stdout_logfile_maxbytes=0
|
||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
; reads snappymail logs
|
|
||||||
[program:snappymail-auth]
|
|
||||||
command=tail -f /snappymail/data/_data_/_default_/logs/auth.log
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
|
|
||||||
[program:snappymail-errors]
|
|
||||||
command=tail -f /snappymail/data/_data_/_default_/logs/errors.log
|
|
||||||
# everything is an error
|
|
||||||
stdout_logfile=/dev/stderr
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
redirect_stderr=true
|
|
||||||
|
|
||||||
[program:logrotate]
|
|
||||||
command=/logrotate-loop.sh
|
|
||||||
autorestart=true
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
|
|
||||||
[eventlistener:subprocess-stopped]
|
[eventlistener:subprocess-stopped]
|
||||||
command=php /listener.php
|
command=php /listener.php
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
user=php-cli
|
user=root
|
||||||
numprocs=1
|
numprocs=1
|
||||||
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL
|
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=unexpected
|
autorestart=unexpected
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
[global]
|
[global]
|
||||||
daemonize = no
|
daemonize = no
|
||||||
|
error_log = /dev/stderr
|
||||||
|
log_buffering = no
|
||||||
|
|
||||||
[default]
|
[default]
|
||||||
listen = /tmp/php-fpm.sock
|
listen = 9000
|
||||||
|
user = www-data
|
||||||
|
listen.owner = www-data
|
||||||
|
listen.group = www-data
|
||||||
pm = ondemand
|
pm = ondemand
|
||||||
pm.max_children = 30
|
pm.max_children = 30
|
||||||
pm.process_idle_timeout = 10s
|
pm.process_idle_timeout = 10s
|
||||||
pm.max_requests = 500
|
pm.max_requests = 500
|
||||||
catch_workers_output = yes
|
catch_workers_output = yes
|
||||||
|
decorate_workers_output = no
|
||||||
chdir = /
|
chdir = /
|
||||||
php_admin_value[error_log] = /tmp/php_error.log
|
pm.status_path = /status
|
||||||
php_admin_value[log_errors] = On
|
php_admin_value[log_errors] = On
|
||||||
php_admin_value[expose_php] = Off
|
php_admin_value[expose_php] = Off
|
||||||
php_admin_value[display_errors] = Off
|
php_admin_value[display_errors] = Off
|
||||||
|
|
|
||||||
|
|
@ -1,293 +0,0 @@
|
||||||
; SnappyMail configuration file
|
|
||||||
; Please don't add custom parameters here, those will be overwritten
|
|
||||||
|
|
||||||
[webmail]
|
|
||||||
; Text displayed as page title
|
|
||||||
title = "SnappyMail Webmail"
|
|
||||||
|
|
||||||
; Text displayed on startup
|
|
||||||
loading_description = "SnappyMail"
|
|
||||||
favicon_url = ""
|
|
||||||
|
|
||||||
; Theme used by default
|
|
||||||
theme = "Default"
|
|
||||||
|
|
||||||
; Allow theme selection on settings screen
|
|
||||||
allow_themes = On
|
|
||||||
allow_user_background = Off
|
|
||||||
|
|
||||||
; Language used by default
|
|
||||||
language = "en"
|
|
||||||
|
|
||||||
; Admin Panel interface language
|
|
||||||
language_admin = "en"
|
|
||||||
|
|
||||||
; Allow language selection on settings screen
|
|
||||||
allow_languages_on_settings = On
|
|
||||||
allow_additional_accounts = On
|
|
||||||
allow_additional_identities = On
|
|
||||||
|
|
||||||
; Number of messages displayed on page by default
|
|
||||||
messages_per_page = 20
|
|
||||||
|
|
||||||
; File size limit (MB) for file upload on compose screen
|
|
||||||
; 0 for unlimited.
|
|
||||||
attachment_size_limit = 2
|
|
||||||
|
|
||||||
[interface]
|
|
||||||
show_attachment_thumbnail = On
|
|
||||||
new_move_to_folder_button = On
|
|
||||||
|
|
||||||
[contacts]
|
|
||||||
; Enable contacts
|
|
||||||
enable = Off
|
|
||||||
allow_sync = Off
|
|
||||||
sync_interval = 20
|
|
||||||
type = "sqlite"
|
|
||||||
pdo_dsn = "host=127.0.0.1;port=3306;dbname=snappymail"
|
|
||||||
pdo_user = "root"
|
|
||||||
pdo_password = ""
|
|
||||||
suggestions_limit = 30
|
|
||||||
|
|
||||||
[security]
|
|
||||||
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
|
||||||
csrf_protection = On
|
|
||||||
custom_server_signature = "SnappyMail"
|
|
||||||
x_frame_options_header = "DENY"
|
|
||||||
x_xss_protection_header = "1; mode=block"
|
|
||||||
openpgp = Off
|
|
||||||
|
|
||||||
; Login and password for web admin panel
|
|
||||||
admin_login = "admin"
|
|
||||||
admin_password = ""
|
|
||||||
admin_totp = ""
|
|
||||||
|
|
||||||
; Access settings
|
|
||||||
allow_admin_panel = On
|
|
||||||
hide_x_mailer_header = On
|
|
||||||
admin_panel_host = ""
|
|
||||||
admin_panel_key = "admin"
|
|
||||||
content_security_policy = ""
|
|
||||||
|
|
||||||
[ssl]
|
|
||||||
; Require verification of SSL certificate used.
|
|
||||||
verify_certificate = Off
|
|
||||||
|
|
||||||
; Allow self-signed certificates. Requires verify_certificate.
|
|
||||||
allow_self_signed = On
|
|
||||||
|
|
||||||
; Location of Certificate Authority file on local filesystem (/etc/ssl/certs/ca-certificates.crt)
|
|
||||||
cafile = ""
|
|
||||||
|
|
||||||
; capath must be a correctly hashed certificate directory. (/etc/ssl/certs/)
|
|
||||||
capath = ""
|
|
||||||
|
|
||||||
; Location of client certificate file (pem format with private key) on local filesystem
|
|
||||||
client_cert = ""
|
|
||||||
|
|
||||||
[capa]
|
|
||||||
composer = On
|
|
||||||
contacts = On
|
|
||||||
settings = On
|
|
||||||
quota = On
|
|
||||||
help = On
|
|
||||||
reload = On
|
|
||||||
search = On
|
|
||||||
search_adv = On
|
|
||||||
x-templates = Off
|
|
||||||
dangerous_actions = On
|
|
||||||
message_actions = On
|
|
||||||
messagelist_actions = On
|
|
||||||
attachments_actions = On
|
|
||||||
|
|
||||||
[login]
|
|
||||||
default_domain = ""
|
|
||||||
|
|
||||||
; Allow language selection on webmail login screen
|
|
||||||
allow_languages_on_login = On
|
|
||||||
determine_user_language = On
|
|
||||||
determine_user_domain = Off
|
|
||||||
hide_submit_button = On
|
|
||||||
login_lowercase = On
|
|
||||||
|
|
||||||
; This option allows webmail to remember the logged in user
|
|
||||||
; once they closed the browser window.
|
|
||||||
;
|
|
||||||
; Values:
|
|
||||||
; "DefaultOff" - can be used, disabled by default;
|
|
||||||
; "DefaultOn" - can be used, enabled by default;
|
|
||||||
; "Unused" - cannot be used
|
|
||||||
sign_me_auto = "DefaultOff"
|
|
||||||
|
|
||||||
[plugins]
|
|
||||||
; Enable plugin support
|
|
||||||
enable = Off
|
|
||||||
|
|
||||||
; List of enabled plugins
|
|
||||||
enabled_list = ""
|
|
||||||
|
|
||||||
[defaults]
|
|
||||||
; Editor mode used by default (Plain, Html, HtmlForced or PlainForced)
|
|
||||||
view_editor_type = "Html"
|
|
||||||
|
|
||||||
; layout: 0 - no preview, 1 - side preview, 2 - bottom preview
|
|
||||||
view_layout = 1
|
|
||||||
view_use_checkboxes = On
|
|
||||||
autologout = 30
|
|
||||||
show_images = Off
|
|
||||||
contacts_autosave = On
|
|
||||||
mail_use_threads = Off
|
|
||||||
allow_draft_autosave = On
|
|
||||||
mail_reply_same_folder = Off
|
|
||||||
|
|
||||||
[logs]
|
|
||||||
; Enable logging
|
|
||||||
enable = Off
|
|
||||||
|
|
||||||
; Logs entire request only if error occured (php requred)
|
|
||||||
write_on_error_only = Off
|
|
||||||
|
|
||||||
; Logs entire request only if php error occured
|
|
||||||
write_on_php_error_only = Off
|
|
||||||
|
|
||||||
; Logs entire request only if request timeout (in seconds) occured.
|
|
||||||
write_on_timeout_only = 0
|
|
||||||
|
|
||||||
; Required for development purposes only.
|
|
||||||
; Disabling this option is not recommended.
|
|
||||||
hide_passwords = On
|
|
||||||
time_zone = "UTC"
|
|
||||||
session_filter = ""
|
|
||||||
|
|
||||||
; Log filename.
|
|
||||||
; For security reasons, some characters are removed from filename.
|
|
||||||
; Allows for pattern-based folder creation (see examples below).
|
|
||||||
;
|
|
||||||
; Patterns:
|
|
||||||
; {date:Y-m-d} - Replaced by pattern-based date
|
|
||||||
; Detailed info: http://www.php.net/manual/en/function.date.php
|
|
||||||
; {user:email} - Replaced by user's email address
|
|
||||||
; If user is not logged in, value is set to "unknown"
|
|
||||||
; {user:login} - Replaced by user's login (the user part of an email)
|
|
||||||
; If user is not logged in, value is set to "unknown"
|
|
||||||
; {user:domain} - Replaced by user's domain name (the domain part of an email)
|
|
||||||
; If user is not logged in, value is set to "unknown"
|
|
||||||
; {user:uid} - Replaced by user's UID regardless of account currently used
|
|
||||||
;
|
|
||||||
; {user:ip}
|
|
||||||
; {request:ip} - Replaced by user's IP address
|
|
||||||
;
|
|
||||||
; Others:
|
|
||||||
; {imap:login} {imap:host} {imap:port}
|
|
||||||
; {smtp:login} {smtp:host} {smtp:port}
|
|
||||||
;
|
|
||||||
; Examples:
|
|
||||||
; filename = "log-{date:Y-m-d}.txt"
|
|
||||||
; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log"
|
|
||||||
; filename = "{user:email}-{date:Y-m-d}.txt"
|
|
||||||
; filename = "syslog"
|
|
||||||
filename = "log-{date:Y-m-d}.txt"
|
|
||||||
|
|
||||||
; Enable auth logging in a separate file (for fail2ban)
|
|
||||||
auth_logging = Off
|
|
||||||
auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt"
|
|
||||||
auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"
|
|
||||||
|
|
||||||
[debug]
|
|
||||||
; Special option required for development purposes
|
|
||||||
enable = Off
|
|
||||||
|
|
||||||
[cache]
|
|
||||||
; The section controls caching of the entire application.
|
|
||||||
;
|
|
||||||
; Enables caching in the system
|
|
||||||
enable = On
|
|
||||||
|
|
||||||
; Additional caching key. If changed, cache is purged
|
|
||||||
index = "v1"
|
|
||||||
|
|
||||||
; Can be: files, APC, memcache, redis (beta)
|
|
||||||
fast_cache_driver = "files"
|
|
||||||
|
|
||||||
; Additional caching key. If changed, fast cache is purged
|
|
||||||
fast_cache_index = "v1"
|
|
||||||
|
|
||||||
; Browser-level cache. If enabled, caching is maintainted without using files
|
|
||||||
http = On
|
|
||||||
|
|
||||||
; Browser-level cache time (seconds, Expires header)
|
|
||||||
http_expires = 3600
|
|
||||||
|
|
||||||
; Caching message UIDs when searching and sorting (threading)
|
|
||||||
server_uids = On
|
|
||||||
|
|
||||||
[labs]
|
|
||||||
allow_prefetch = Off
|
|
||||||
cache_system_data = On
|
|
||||||
date_from_headers = On
|
|
||||||
autocreate_system_folders = Off
|
|
||||||
allow_message_append = Off
|
|
||||||
login_fault_delay = 1
|
|
||||||
log_ajax_response_write_limit = 300
|
|
||||||
allow_html_editor_biti_buttons = Off
|
|
||||||
allow_ctrl_enter_on_compose = On
|
|
||||||
try_to_detect_hidden_images = Off
|
|
||||||
use_app_debug_js = Off
|
|
||||||
use_mobile_version_for_tablets = Off
|
|
||||||
use_app_debug_css = Off
|
|
||||||
use_imap_sort = On
|
|
||||||
use_imap_force_selection = Off
|
|
||||||
use_imap_list_subscribe = On
|
|
||||||
use_imap_thread = On
|
|
||||||
use_imap_move = Off
|
|
||||||
use_imap_expunge_all_on_delete = Off
|
|
||||||
imap_forwarded_flag = "$Forwarded"
|
|
||||||
imap_read_receipt_flag = "$ReadReceipt"
|
|
||||||
imap_body_text_limit = 555000
|
|
||||||
imap_message_list_fast_simple_search = On
|
|
||||||
imap_message_list_count_limit_trigger = 0
|
|
||||||
imap_message_list_date_filter = 0
|
|
||||||
imap_message_list_permanent_filter = ""
|
|
||||||
imap_message_all_headers = Off
|
|
||||||
imap_large_thread_limit = 50
|
|
||||||
imap_folder_list_limit = 200
|
|
||||||
imap_show_login_alert = On
|
|
||||||
imap_use_list_status = On
|
|
||||||
imap_timeout = 300
|
|
||||||
smtp_timeout = 60
|
|
||||||
smtp_show_server_errors = Off
|
|
||||||
sieve_utf8_folder_name = On
|
|
||||||
sieve_auth_plain_initial = On
|
|
||||||
sieve_allow_fileinto_inbox = Off
|
|
||||||
sieve_timeout = 10
|
|
||||||
sasl_allow_plain = On
|
|
||||||
sasl_allow_cram_md5 = Off
|
|
||||||
domain_list_limit = 99
|
|
||||||
mail_func_clear_headers = On
|
|
||||||
mail_func_additional_parameters = Off
|
|
||||||
favicon_status = On
|
|
||||||
folders_spec_limit = 50
|
|
||||||
curl_proxy = ""
|
|
||||||
curl_proxy_auth = ""
|
|
||||||
in_iframe = Off
|
|
||||||
force_https = Off
|
|
||||||
http_client_ip_check_proxy = Off
|
|
||||||
fast_cache_memcache_host = "127.0.0.1"
|
|
||||||
fast_cache_memcache_port = 11211
|
|
||||||
fast_cache_redis_host = "127.0.0.1"
|
|
||||||
fast_cache_redis_port = 6379
|
|
||||||
use_local_proxy_for_external_images = On
|
|
||||||
detect_image_exif_orientation = On
|
|
||||||
cookie_default_path = ""
|
|
||||||
cookie_default_secure = Off
|
|
||||||
check_new_messages = On
|
|
||||||
replace_env_in_configuration = ""
|
|
||||||
strict_html_parser = Off
|
|
||||||
boundary_prefix = ""
|
|
||||||
kolab_enabled = Off
|
|
||||||
dev_email = ""
|
|
||||||
dev_password = ""
|
|
||||||
|
|
||||||
[version]
|
|
||||||
current = "2.12.2"
|
|
||||||
saved = "Mon, 23 Aug 2021 07:55:13 +0000"
|
|
||||||
7
.docker/release/test/build_and_test.sh
Executable file
7
.docker/release/test/build_and_test.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# This script uses docker buildx to build a docker image, and loads it into the docker daemon. Then it executes ./test.sh to test the docker image
|
||||||
|
# It is useful for testing release builds in development
|
||||||
|
set -eu
|
||||||
|
IMAGE=snappymail/snappymail:test
|
||||||
|
DOCKER_BUILDX=1 docker build -t "$IMAGE" -f .docker/release/Dockerfile .
|
||||||
|
.docker/release/test/test.sh "$IMAGE"
|
||||||
29
.docker/release/test/config.yaml
Normal file
29
.docker/release/test/config.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# See: https://github.com/GoogleContainerTools/container-structure-test
|
||||||
|
# See: https://github.com/GoogleContainerTools/container-structure-test/issues/78
|
||||||
|
schemaVersion: 2.0.0
|
||||||
|
commandTests:
|
||||||
|
- name: Integration test
|
||||||
|
command: /bin/sh
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -eux
|
||||||
|
DEBUG=true /entrypoint.sh > /tmp/test 2>&1 &
|
||||||
|
sleep 5
|
||||||
|
cat /tmp/test
|
||||||
|
pidof supervisord
|
||||||
|
pidof nginx
|
||||||
|
pidof php-fpm
|
||||||
|
ls -al /var/lib/snappymail/_data_/_default_/configs/application.ini
|
||||||
|
ls -al /var/lib/snappymail/_data_/_default_/admin_password.txt
|
||||||
|
nc -vz 127.0.0.1 8888
|
||||||
|
nc -vz 127.0.0.1 9000
|
||||||
|
wget -S -T 3 -O /dev/null http://127.0.0.1:8888
|
||||||
|
kill `pidof supervisord`
|
||||||
|
metadataTest:
|
||||||
|
exposedPorts: ["8888", "9000"]
|
||||||
|
volumes: ["/var/lib/snappymail"]
|
||||||
|
entrypoint: []
|
||||||
|
cmd: ["/entrypoint.sh"]
|
||||||
|
workdir: /snappymail
|
||||||
|
user: root
|
||||||
10
.docker/release/test/test.sh
Executable file
10
.docker/release/test/test.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# This script tests a given docker image using https://github.com/GoogleContainerTools/container-structure-test
|
||||||
|
set -eu
|
||||||
|
SCRIPT_DIR=$( cd "$(dirname "$0")" && pwd )
|
||||||
|
IMAGE=${1:-}
|
||||||
|
echo "Testing image: $IMAGE"
|
||||||
|
docker run --rm -i \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
-v "$SCRIPT_DIR/config.yaml:/config.yaml:ro" \
|
||||||
|
gcr.io/gcp-runtimes/container-structure-test:latest test --image "$IMAGE" --config config.yaml
|
||||||
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/.git
|
||||||
|
/node_modules
|
||||||
16
.eslintrc.js
16
.eslintrc.js
|
|
@ -3,7 +3,7 @@ module.exports = {
|
||||||
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
||||||
extends: ['eslint:recommended'],
|
extends: ['eslint:recommended'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 6,
|
ecmaVersion: 11,
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -27,8 +27,6 @@ module.exports = {
|
||||||
'SquireUI': "readonly",
|
'SquireUI': "readonly",
|
||||||
// node_modules/knockout but dev/External/ko.js is used
|
// node_modules/knockout but dev/External/ko.js is used
|
||||||
'ko': "readonly",
|
'ko': "readonly",
|
||||||
// dev/External/ifvisible.js
|
|
||||||
'ifvisible': "readonly",
|
|
||||||
// vendors/routes/
|
// vendors/routes/
|
||||||
'hasher': "readonly",
|
'hasher': "readonly",
|
||||||
'Crossroads': "readonly",
|
'Crossroads': "readonly",
|
||||||
|
|
@ -37,10 +35,17 @@ module.exports = {
|
||||||
// vendors/bootstrap/bootstrap.native.js
|
// vendors/bootstrap/bootstrap.native.js
|
||||||
'BSN': "readonly",
|
'BSN': "readonly",
|
||||||
// Mailvelope
|
// Mailvelope
|
||||||
'mailvelope': "readonly"
|
'mailvelope': "readonly",
|
||||||
|
// Punycode
|
||||||
|
'IDN': "readonly",
|
||||||
|
// Turndown
|
||||||
|
'TurndownService': "readonly",
|
||||||
|
// Marked
|
||||||
|
'marked': "readonly"
|
||||||
},
|
},
|
||||||
// http://eslint.org/docs/rules/
|
// http://eslint.org/docs/rules/
|
||||||
rules: {
|
rules: {
|
||||||
|
'no-cond-assign': 0,
|
||||||
// plugins
|
// plugins
|
||||||
'no-mixed-spaces-and-tabs': 'off',
|
'no-mixed-spaces-and-tabs': 'off',
|
||||||
'max-len': [
|
'max-len': [
|
||||||
|
|
@ -53,6 +58,7 @@ module.exports = {
|
||||||
ignoreTrailingComments: true,
|
ignoreTrailingComments: true,
|
||||||
ignorePattern: '(^\\s*(const|let|var)\\s.+=\\s*require\\s*\\(|^import\\s.+\\sfrom\\s.+;$)'
|
ignorePattern: '(^\\s*(const|let|var)\\s.+=\\s*require\\s*\\(|^import\\s.+\\sfrom\\s.+;$)'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
'no-constant-condition': ["error", { "checkLoops": false }]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,2 +1,2 @@
|
||||||
community_bridge: SnappyMail
|
github: the-djmaze
|
||||||
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
||||||
|
|
|
||||||
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -23,13 +23,16 @@ A clear and concise description of what you expected to happen.
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Please complete the following information:**
|
||||||
- Browser [e.g. chromium 89, firefox 86, safari 14]
|
- Browser: [e.g. chromium 100, firefox 100, safari 14, mobile]
|
||||||
- SnappyMail Version [e.g. 2.3.4]
|
- IMAP daemon: [e.g. courier, dovecot]
|
||||||
|
- PHP version:
|
||||||
|
- SnappyMail Version:
|
||||||
|
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
|
||||||
|
|
||||||
**Mobile (please complete the following information):**
|
**Debug/logging information**
|
||||||
- Browser [e.g. stock browser, chrome 87, firefox 86, safari 14]
|
[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)
|
||||||
- SnappyMail Version [e.g. 2.3.4]
|
- [ ] I've placed them here (few lines) or as attachments (many lines)
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
|
||||||
93
.github/workflows/docker-pr.yml
vendored
Normal file
93
.github/workflows/docker-pr.yml
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
name: docker-pr
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# This step generates the docker tags
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
env:
|
||||||
|
# This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
djmaze/snappymail
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
# type=ref,event=pr generates tag(s) on PRs only. E.g. 'pr-123', 'pr-123-abc0123'
|
||||||
|
tags: |
|
||||||
|
type=ref,event=pr
|
||||||
|
type=ref,suffix=-{{sha}},event=pr
|
||||||
|
# The rest of the org.opencontainers.image.xxx labels are dynamically generated
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.description=SnappyMail
|
||||||
|
org.opencontainers.image.licenses=AGPLv3
|
||||||
|
|
||||||
|
# See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
|
# See: https://github.com/docker/buildx/issues/59
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: '.'
|
||||||
|
file: ./.docker/release/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
|
- name: Docker images
|
||||||
|
run: |
|
||||||
|
docker images
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 )
|
||||||
|
.docker/release/test/test.sh "$TAG"
|
||||||
|
|
||||||
|
- name: Build all archs
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: '.'
|
||||||
|
file: ./.docker/release/Dockerfile
|
||||||
|
platforms: linux/386,linux/amd64,linux/arm64
|
||||||
|
push: false
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
|
# Temp fix
|
||||||
|
# https://github.com/docker/build-push-action/issues/252
|
||||||
|
# https://github.com/moby/buildkit/issues/1896
|
||||||
|
- name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache
|
||||||
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||||
117
.github/workflows/docker.yml
vendored
Normal file
117
.github/workflows/docker.yml
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
name: docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v2.*'
|
||||||
|
|
||||||
|
# This is needed to push to GitHub Container Registry. See https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# This step generates the docker tags
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
env:
|
||||||
|
# This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
djmaze/snappymail
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
# type=ref,event=branch generates tag(s) on branch only. E.g. 'master', 'master-abc0123'
|
||||||
|
# type=ref,event=tag generates tag(s) on tags only. E.g. 'v0.0.0', 'v0.0.0-abc0123', and 'latest'
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
# The rest of the org.opencontainers.image.xxx labels are dynamically generated
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.description=SnappyMail
|
||||||
|
org.opencontainers.image.licenses=AGPLv3
|
||||||
|
|
||||||
|
# See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
|
- name: Login to Docker Hub registry
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') # Login only on tags
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') # Login only on tags
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# See: https://github.com/docker/buildx/issues/59
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: '.'
|
||||||
|
file: ./.docker/release/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
|
- name: Docker images
|
||||||
|
run: |
|
||||||
|
docker images
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 )
|
||||||
|
.docker/release/test/test.sh "$TAG"
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build-and-push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: '.'
|
||||||
|
file: ./.docker/release/Dockerfile
|
||||||
|
# TODO: Add more arches?
|
||||||
|
# platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
|
||||||
|
platforms: linux/386,linux/amd64,linux/arm64
|
||||||
|
push: ${{ startsWith(github.ref, 'refs/tags/') }} # Push only on tags
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
|
# Temp fix
|
||||||
|
# https://github.com/docker/build-push-action/issues/252
|
||||||
|
# https://github.com/moby/buildkit/issues/1896
|
||||||
|
- name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache
|
||||||
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -12,10 +12,12 @@
|
||||||
/.docker/dev/mail/config
|
/.docker/dev/mail/config
|
||||||
/.docker/dev/nginx/ssl/*
|
/.docker/dev/nginx/ssl/*
|
||||||
!/.docker/dev/nginx/ssl/.gitempty
|
!/.docker/dev/nginx/ssl/.gitempty
|
||||||
|
/.docker/release/snappymail-*.zip
|
||||||
/dist
|
/dist
|
||||||
/data
|
/data
|
||||||
/MULTIPLY
|
/MULTIPLY
|
||||||
/include.php
|
/include.php
|
||||||
|
/issues
|
||||||
.idea/
|
.idea/
|
||||||
.env
|
.env
|
||||||
/test
|
/test
|
||||||
|
|
@ -23,6 +25,8 @@
|
||||||
/vendors/knockout/spec
|
/vendors/knockout/spec
|
||||||
/vendors/openpgp-5
|
/vendors/openpgp-5
|
||||||
!/vendors/openpgp-5/dist
|
!/vendors/openpgp-5/dist
|
||||||
|
/vendors/squire2
|
||||||
|
!/vendors/squire2/dist
|
||||||
/vendors/vanillaqr.js/
|
/vendors/vanillaqr.js/
|
||||||
/integrations/nextcloud/rainloop
|
/integrations/nextcloud/rainloop
|
||||||
/integrations/owncloud/rainloop
|
/integrations/owncloud/rainloop
|
||||||
|
|
|
||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
|
|
@ -1,3 +1,7 @@
|
||||||
[submodule "vendors/openpgp-5"]
|
[submodule "vendors/openpgp-5"]
|
||||||
path = vendors/openpgp-5
|
path = vendors/openpgp-5
|
||||||
url = git@github.com:the-djmaze/openpgpjs.git
|
url = git@github.com:the-djmaze/openpgpjs.git
|
||||||
|
[submodule "vendors/squire2"]
|
||||||
|
path = vendors/squire2
|
||||||
|
url = git@github.com:the-djmaze/Squire.git
|
||||||
|
branch = snappymail
|
||||||
|
|
|
||||||
11
.htaccess
11
.htaccess
|
|
@ -1,7 +1,15 @@
|
||||||
|
<FilesMatch "index\.php">
|
||||||
|
AcceptPathInfo On
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
<IfModule mod_rewrite.c>
|
<IfModule mod_rewrite.c>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
# Redirect cPanel
|
# Redirect cPanel
|
||||||
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
<IfModule mod_expires.c>
|
<IfModule mod_expires.c>
|
||||||
|
|
@ -23,10 +31,9 @@
|
||||||
# Header set Cache-Control "public, max-age=31536000"
|
# Header set Cache-Control "public, max-age=31536000"
|
||||||
# Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
|
# Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
|
||||||
# Header set Referrer-Policy "no-referrer"
|
# Header set Referrer-Policy "no-referrer"
|
||||||
Header set Strict-Transport-Security "max-age=31536000"
|
# Header set Strict-Transport-Security "max-age=31536000"
|
||||||
Header set imagetoolbar "no"
|
Header set imagetoolbar "no"
|
||||||
# Header set X-Content-Type-Options "nosniff"
|
# Header set X-Content-Type-Options "nosniff"
|
||||||
# Header set X-Frame-Options "DENY"
|
|
||||||
# Header set X-XSS-Protection "1; mode=block"
|
# Header set X-XSS-Protection "1; mode=block"
|
||||||
Header set Service-Worker-Allowed "/"
|
Header set Service-Worker-Allowed "/"
|
||||||
|
|
||||||
|
|
|
||||||
2422
CHANGELOG.md
Normal file
2422
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -29,7 +29,7 @@
|
||||||
**Editing HTML Template Files**
|
**Editing HTML Template Files**
|
||||||
|
|
||||||
1. Edit data/\_data_/\_default_/configs/application.ini
|
1. Edit data/\_data_/\_default_/configs/application.ini
|
||||||
2. Set 'cache_system_data' to Off
|
2. Set `[cache] system_data` to Off
|
||||||
|
|
||||||
**Release**
|
**Release**
|
||||||
|
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -658,4 +658,4 @@ specific requirements.
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -61,8 +61,6 @@ rl-watch-js:
|
||||||
|
|
||||||
rl-build:
|
rl-build:
|
||||||
@docker compose run --no-deps --rm node gulp all
|
@docker compose run --no-deps --rm node gulp all
|
||||||
rl-build-pro:
|
|
||||||
@docker compose run --no-deps --rm node gulp all --pro
|
|
||||||
|
|
||||||
yarn-install:
|
yarn-install:
|
||||||
@docker compose run --no-deps --rm node yarn install
|
@docker compose run --no-deps --rm node yarn install
|
||||||
|
|
|
||||||
92
README.md
92
README.md
|
|
@ -5,6 +5,10 @@
|
||||||
<br>
|
<br>
|
||||||
<h1>SnappyMail</h1>
|
<h1>SnappyMail</h1>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
[](https://github.com/the-djmaze/snappymail/actions/workflows/docker.yml)
|
||||||
|
[](https://hub.docker.com/r/djmaze/snappymail/tags)
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Simple, modern, lightweight & fast web-based email client.
|
Simple, modern, lightweight & fast web-based email client.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -26,7 +30,7 @@ For more information about the product, check [snappymail.eu](https://snappymail
|
||||||
|
|
||||||
Information about installing the product, check the [wiki page](https://github.com/the-djmaze/snappymail/wiki/Installation-instructions).
|
Information about installing the product, check the [wiki page](https://github.com/the-djmaze/snappymail/wiki/Installation-instructions).
|
||||||
|
|
||||||
And don't forget to read the [RainLoop documentation](https://www.rainloop.net/docs/).
|
And don't forget to read the whole [Wiki](https://github.com/the-djmaze/snappymail/wiki).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
@ -34,19 +38,19 @@ And don't forget to read the [RainLoop documentation](https://www.rainloop.net/d
|
||||||
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
|
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
|
||||||
http://www.gnu.org/licenses/agpl-3.0.html
|
http://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
Copyright (c) 2020 - 2022 SnappyMail
|
Copyright (c) 2020 - 2024 SnappyMail
|
||||||
Copyright (c) 2013 - 2021 RainLoop
|
Copyright (c) 2013 - 2022 RainLoop
|
||||||
|
|
||||||
## Modifications
|
## Modifications
|
||||||
|
|
||||||
This fork of RainLoop has the following changes:
|
This fork of RainLoop has the following changes:
|
||||||
|
|
||||||
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, OwnCloud, X-Mailer)
|
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, X-Mailer)
|
||||||
* Admin uses password_hash/password_verify
|
* Admin uses password_hash/password_verify
|
||||||
* Auth failed attempts written to syslog
|
* Auth failed attempts written to syslog
|
||||||
* Added Fail2ban instructions
|
* Added Fail2ban instructions
|
||||||
* ES2018
|
* ES2020
|
||||||
* PHP 7.3+ required
|
* PHP 7.4+ required
|
||||||
* PHP mbstring extension required
|
* PHP mbstring extension required
|
||||||
* PHP replaced pclZip with PharData and ZipArchive
|
* PHP replaced pclZip with PharData and ZipArchive
|
||||||
* Dark mode
|
* Dark mode
|
||||||
|
|
@ -57,12 +61,15 @@ This fork of RainLoop has the following changes:
|
||||||
* Removed background video support
|
* Removed background video support
|
||||||
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
||||||
* Removed Spyc yaml
|
* Removed Spyc yaml
|
||||||
|
* Removed OwnCloud
|
||||||
* Replaced gulp-uglify with gulp-terser
|
* Replaced gulp-uglify with gulp-terser
|
||||||
* CRLF => LF line endings
|
* CRLF => LF line endings
|
||||||
* Embed boot.js and boot.css into index.html
|
* Embed boot.js and boot.css into index.html
|
||||||
* Ongoing removal of old JavaScript code (things are native these days)
|
* Removal of old JavaScript code (things are native these days)
|
||||||
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
* Added modified [Squire](https://github.com/the-djmaze/Squire/tree/snappymail) HTML editor as replacement for CKEditor
|
||||||
|
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
||||||
* Split Admin specific JavaScript code from User code
|
* Split Admin specific JavaScript code from User code
|
||||||
|
* Split Sieve specific JavaScript code from User code
|
||||||
* JSON reviver
|
* JSON reviver
|
||||||
* Better memory garbage collection management
|
* Better memory garbage collection management
|
||||||
* Added serviceworker for Notifications
|
* Added serviceworker for Notifications
|
||||||
|
|
@ -73,16 +80,10 @@ This fork of RainLoop has the following changes:
|
||||||
* Added support to load plugins as .phar
|
* Added support to load plugins as .phar
|
||||||
* Replaced old Sabre library
|
* Replaced old Sabre library
|
||||||
* AddressBook Contacts support MySQL/MariaDB utf8mb4
|
* AddressBook Contacts support MySQL/MariaDB utf8mb4
|
||||||
* Prevent Google FLoC
|
|
||||||
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
||||||
* Reduced excessive DOM size
|
* Reduced excessive DOM size
|
||||||
* Support [Kolab groupware](https://kolab.org/)
|
* Support [Kolab groupware](https://kolab.org/)
|
||||||
* Support IMAP RFC 2971 ID extension
|
* Support many more [IMAP RFC's](https://snappymail.eu/comparison#IMAP)
|
||||||
* Support IMAP RFC 5258 LIST-EXTENDED
|
|
||||||
* Support IMAP RFC 5464 METADATA
|
|
||||||
* Support IMAP RFC 5819 LIST-STATUS
|
|
||||||
* Support IMAP RFC 7628 SASL OAUTHBEARER aka XOAUTH2
|
|
||||||
* Support IMAP4rev2 RFC 9051
|
|
||||||
* Support Sodium and OpenSSL for encryption
|
* Support Sodium and OpenSSL for encryption
|
||||||
* Much better PGP support
|
* Much better PGP support
|
||||||
|
|
||||||
|
|
@ -92,11 +93,11 @@ This fork of RainLoop has the following changes:
|
||||||
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy.
|
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy.
|
||||||
Supported are:
|
Supported are:
|
||||||
|
|
||||||
* Chrome 69+
|
* Chrome 80+
|
||||||
* Edge 79+
|
* Edge 80+
|
||||||
* Firefox 69+
|
* Firefox 78+
|
||||||
* Opera 56+
|
* Opera 67+
|
||||||
* Safari 12+
|
* Safari 13.1+
|
||||||
|
|
||||||
|
|
||||||
### Removal of old JavaScript
|
### Removal of old JavaScript
|
||||||
|
|
@ -104,10 +105,8 @@ Supported are:
|
||||||
The result is faster and smaller download code (good for mobile networks).
|
The result is faster and smaller download code (good for mobile networks).
|
||||||
|
|
||||||
* Added dev/prototype.js for some additional features
|
* Added dev/prototype.js for some additional features
|
||||||
* boot.js without webpack overhead
|
|
||||||
* Modified Jua.js to be without jQuery
|
* Modified Jua.js to be without jQuery
|
||||||
* Replaced Autolinker with simple https/email detection
|
* Replaced Autolinker with simple https/email detection
|
||||||
* Replaced ifvisible.js with simple drop-in replacement
|
|
||||||
* Replaced momentToNode with proper HTML5 `<time>`
|
* Replaced momentToNode with proper HTML5 `<time>`
|
||||||
* Replaced resize listeners with ResizeObserver
|
* Replaced resize listeners with ResizeObserver
|
||||||
* Replaced bootstrap.js with native drop-in replacement
|
* Replaced bootstrap.js with native drop-in replacement
|
||||||
|
|
@ -118,6 +117,8 @@ The result is faster and smaller download code (good for mobile networks).
|
||||||
* Replaced simplestatemanager with CSS @media
|
* Replaced simplestatemanager with CSS @media
|
||||||
* Replaced inputosaurus with own code
|
* Replaced inputosaurus with own code
|
||||||
* Replaced keymaster with own shortcuts handler
|
* Replaced keymaster with own shortcuts handler
|
||||||
|
* Replaced OpenPGP.js v2 with OpenPGP.js v5
|
||||||
|
* Removed ifvisible.js
|
||||||
* Removed pikaday
|
* Removed pikaday
|
||||||
* Removed underscore
|
* Removed underscore
|
||||||
* Removed polyfills
|
* Removed polyfills
|
||||||
|
|
@ -135,29 +136,32 @@ The result is faster and smaller download code (good for mobile networks).
|
||||||
* Removed ProgressJS
|
* Removed ProgressJS
|
||||||
|
|
||||||
|
|
||||||
RainLoop 1.15 vs SnappyMail
|
RainLoop 1.17 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.158.025 | 86.330 |
|
|admin.js |2.170.153 | 84.925 |
|
||||||
|app.js |4.215.733 | 442.968 |
|
|app.js |4.207.787 | 447.263 |
|
||||||
|boot.js | 672.433 | 2.049 |
|
|boot.js | 868.735 | 4.343 |
|
||||||
|libs.js | 647.679 | 206.717 |
|
|libs.js | 658.812 | 233.728 |
|
||||||
|polyfills.js | 325.908 | 0 |
|
|sieve.js | 0 | 91.418 |
|
||||||
|
|polyfills.js | 334.608 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.019.778 | 738.349 |
|
|TOTAL |8.240.095 | 861.962 |
|
||||||
|
|
||||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||||
|admin.min.js | 255.514 | 43.341 | 73.899 | 13.994 | 60.674 | 12.482 |
|
|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
|
||||||
|app.min.js | 516.000 | 219.211 |140.430 | 68.665 |110.657 | 58.612 |
|
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|
||||||
|boot.min.js | 66.456 | 1.278 | 22.553 | 789 | 20.043 | 632 |
|
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|
||||||
|libs.min.js | 574.626 | 98.988 |177.280 | 36.290 |151.855 | 32.467 |
|
|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
|
||||||
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|
|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|
||||||
|TOTAL |1.445.204 | 362.818 |425.477 |119.738 |353.301 |104.193 |
|
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||||
|TOTAL (no admin) |1.189.690 | 319.477 |351.061 |105.744 |292.627 | 91.711 |
|
|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
|
||||||
|
|TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 |
|
||||||
|
|TOTAL admin | 959.099 | 154.596 |292.911 | 54.811 |249.877 | 48.841 |
|
||||||
|
|
||||||
For a user its around 69% smaller and faster than traditional RainLoop.
|
For a user it is around 66% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
### CSS changes
|
### CSS changes
|
||||||
|
|
||||||
|
|
@ -165,6 +169,7 @@ For a user its around 69% smaller and faster than traditional RainLoop.
|
||||||
* Themes work in mobile mode
|
* Themes work in mobile mode
|
||||||
* Bugfix invalid/conflicting css rules
|
* Bugfix invalid/conflicting css rules
|
||||||
* Use flexbox
|
* Use flexbox
|
||||||
|
* Use border-box
|
||||||
* Split app.css to have separate admin.css
|
* Split app.css to have separate admin.css
|
||||||
* Remove oldschool 'float'
|
* Remove oldschool 'float'
|
||||||
* Remove unused css
|
* Remove unused css
|
||||||
|
|
@ -183,12 +188,12 @@ For a user its around 69% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||||
|------------ |-------: |------: |------: |------: |--------: |
|
|------------ |-------: |------: |------: |------: |--------: |
|
||||||
|app.css | 340.334 | 82.315 | 46.959 | 16.904 | 14.555 |
|
|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
|
||||||
|app.min.css | 274.791 | 66.384 | 39.618 | 14.984 | 13.193 |
|
|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 1.326 | | 664 | 545 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||||
|admin.css | | 30.212 | | 6.840 | 5.929 |
|
|admin.css | | 30.880 | | 7.045 | 6.127 |
|
||||||
|admin.min.css | | 24.288 | | 6.195 | 5.451 |
|
|admin.min.css | | 24.959 | | 6.368 | 5.615 |
|
||||||
|
|
||||||
### PGP
|
### PGP
|
||||||
RainLoop uses the old OpenPGP.js v2
|
RainLoop uses the old OpenPGP.js v2
|
||||||
|
|
@ -202,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|
||||||
|
|
||||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
||||||
|openpgp.min.js | 330.742 | 539.642 |102.388 | 167.112 | 84.241 | 137.447 |
|
|openpgp.min.js | 330.742 | 546.165 |102.388 | 169.207 | 84.241 | 138.688 |
|
||||||
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -212,7 +217,6 @@ The [Squire](https://github.com/neilj/Squire) implementation is not 100% compati
|
||||||
Still TODO:
|
Still TODO:
|
||||||
|
|
||||||
* support for tables (really needed?!?)
|
* support for tables (really needed?!?)
|
||||||
* support BIDI (really needed?!?)
|
|
||||||
|
|
||||||
| | normal | min | gzip | min gzip |
|
| | normal | min | gzip | min gzip |
|
||||||
|-------- |-------: |-------: |------: |--------: |
|
|-------- |-------: |-------: |------: |--------: |
|
||||||
|
|
@ -220,5 +224,3 @@ Still TODO:
|
||||||
|ckeditor | ? | 520.035 | ? | 155.916 |
|
|ckeditor | ? | 520.035 | ? | 155.916 |
|
||||||
|
|
||||||
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
|
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
|
||||||
|
|
||||||
To use the old CKEditor, you must install the plugin.
|
|
||||||
|
|
|
||||||
40
SECURITY.md
Normal file
40
SECURITY.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Currently due to the fast development only the latest version receives security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| -------- | --------- |
|
||||||
|
| 2.34.x | ✔ |
|
||||||
|
| < 2.34.0 | ❌ |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues or vulnerabilities as an encrypted email to [security@snappymail.eu](mailto:security@snappymail.eu).
|
||||||
|
Your report should be detailed enough with clear steps to reproduce and classify the found vulnerability.
|
||||||
|
|
||||||
|
You can find the PGP public key below and on the major public keyservers like [keys.openpgp.org](https://keys.openpgp.org).
|
||||||
|
```
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
Comment: Type: 255-bit EdDSA
|
||||||
|
Comment: Fingerprint: 445D265124E6072671E64D0733F868A7E35E8277
|
||||||
|
|
||||||
|
mDMEYiE2QRYJKwYBBAHaRw8BAQdAqMrQUm6DddWcQNo0VEjNIu3Q6CfP3nokVv2Y
|
||||||
|
rNQ1avq0LFNuYXBweU1haWwgU2VjdXJpdHkgPHNlY3VyaXR5QHNuYXBweW1haWwu
|
||||||
|
ZXU+iJQEExYKADwWIQREXSZRJOYHJnHmTQcz+Gin416CdwUCYiE2QQIbAwULCQgH
|
||||||
|
AgMiAgEGFQoJCAsCBBYCAwECHgcCF4AACgkQM/hop+NegnfSGgD9GEHpOrvWpBGY
|
||||||
|
dYfvVd/+Lv5d+dFBcPyki9zu9zHfhwkBAL343EF6ZR0XwMlOQu9wu0hT9KBz4g55
|
||||||
|
6D41i0PrEaoBuDgEYiE2QRIKKwYBBAGXVQEFAQEHQMMr9gcVcJ3aiup/tpl8ZXxy
|
||||||
|
aJiJRGkPyNwGI5vxHMpZAwEIB4h4BBgWCgAgFiEERF0mUSTmByZx5k0HM/hop+Ne
|
||||||
|
gncFAmIhNkECGwwACgkQM/hop+NegndVhgD/SVGSKbF4G2W024VpW2tm3zCT+ue+
|
||||||
|
YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
|
||||||
|
=nSOi
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing and Credits
|
||||||
|
|
||||||
|
I will analyze and fix the reported issue as fast as possible.
|
||||||
|
Together with the reporter, I plan the disclosure of the found and fixed vulnerability.
|
||||||
|
Credits to the reporter are granted and can be included in all public communication if desired.
|
||||||
15
_include.php
15
_include.php
|
|
@ -4,16 +4,6 @@
|
||||||
|
|
||||||
//header('Strict-Transport-Security: max-age=31536000');
|
//header('Strict-Transport-Security: max-age=31536000');
|
||||||
|
|
||||||
/**
|
|
||||||
* Uncomment to use gzip compressed output
|
|
||||||
*/
|
|
||||||
//define('USE_GZIP', 1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uncomment to use brotli compressed output
|
|
||||||
*/
|
|
||||||
//define('USE_BROTLI', 1);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uncomment to enable multiple domain installation.
|
* Uncomment to enable multiple domain installation.
|
||||||
*/
|
*/
|
||||||
|
|
@ -34,3 +24,8 @@
|
||||||
* Additional configuration file name
|
* Additional configuration file name
|
||||||
*/
|
*/
|
||||||
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
|
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Also update extensions on upgrade
|
||||||
|
*/
|
||||||
|
//define('SNAPPYMAIL_UPDATE_PLUGINS', 1);
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<IfModule mod_headers.c>
|
|
||||||
<IfModule mod_rewrite.c>
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
RewriteCond %{HTTP:Accept-encoding} br
|
|
||||||
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
|
||||||
RewriteRule "^(.+\.js)$" "$1\.br" [L,T=application/javascript,QSA]
|
|
||||||
|
|
||||||
RewriteCond %{HTTP:Accept-encoding} gzip
|
|
||||||
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
|
|
||||||
RewriteRule "^(.+\.js)$" "$1\.gz" [L,T=application/javascript,QSA]
|
|
||||||
|
|
||||||
RewriteCond %{HTTP:Accept-encoding} br
|
|
||||||
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
|
||||||
RewriteRule "^(.+\.css)$" "$1\.br" [L,T=text/css,QSA]
|
|
||||||
|
|
||||||
RewriteCond %{HTTP:Accept-encoding} gzip
|
|
||||||
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
|
|
||||||
RewriteRule "^(.+\.css)$" "$1\.gz" [L,T=text/css,QSA]
|
|
||||||
|
|
||||||
<FilesMatch "(\.js\.br|\.css\.br)$">
|
|
||||||
SetEnv no-gzip 1
|
|
||||||
SetEnv no-brotli 1
|
|
||||||
# Serve correct encoding type.
|
|
||||||
Header append Content-Encoding br
|
|
||||||
# Force proxies to cache brotli &
|
|
||||||
# non-brotli css/js files separately.
|
|
||||||
Header append Vary Accept-Encoding
|
|
||||||
</FilesMatch>
|
|
||||||
|
|
||||||
<FilesMatch "(\.js\.gz|\.css\.gz)$">
|
|
||||||
SetEnv no-gzip 1
|
|
||||||
SetEnv no-brotli 1
|
|
||||||
# Serve correct encoding type.
|
|
||||||
Header append Content-Encoding gzip
|
|
||||||
# Force proxies to cache gzipped &
|
|
||||||
# non-gzipped css/js files separately.
|
|
||||||
Header append Vary Accept-Encoding
|
|
||||||
</FilesMatch>
|
|
||||||
|
|
||||||
<FilesMatch "(\.js\.br|\.js\.gz)$">
|
|
||||||
Header set Content-Type "application/javascript; charset=utf-8"
|
|
||||||
ForceType application/javascript
|
|
||||||
</FilesMatch>
|
|
||||||
<FilesMatch "(\.css\.br|\.css\.gz)$">
|
|
||||||
Header set Content-Type "text/css; charset=utf-8"
|
|
||||||
ForceType text/css
|
|
||||||
</FilesMatch>
|
|
||||||
</IfModule>
|
|
||||||
</IfModule>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
9
build/SnappyMail.asc
Normal file
9
build/SnappyMail.asc
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mDMEYg0atBYJKwYBBAHaRw8BAQdA2S2tvGavChACjtBastsKRThD3rsBW1LUZLmN
|
||||||
|
Zbs4uaG0I1NuYXBweU1haWwgPHJlbGVhc2VzQHNuYXBweW1haWwuZXU+iJQEExYK
|
||||||
|
ADwWIQQQFuRweRRVQvi6EzVIIIuhMpDz6wUCYg0atAIbAwULCQgHAgMiAgEGFQoJ
|
||||||
|
CAsCBBYCAwECHgcCF4AACgkQSCCLoTKQ8+u9SAD/Q/IoAwjUkKDJBPq0RGwCFnl6
|
||||||
|
FG/VHB97CvBSpGOxtIsBAMCwMhWlsaBHAEqbzxiN+cdlMYwV23+SWLUJ/XMFgukE
|
||||||
|
=5ZwF
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
38
build/cpanel.php
Executable file
38
build/cpanel.php
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
echo "\x1b[33;1m === cPanel === \x1b[0m\n";
|
||||||
|
|
||||||
|
$cpanel_destination = "{$destPath}snappymail-{$package->version}-cpanel.tar";
|
||||||
|
|
||||||
|
@unlink($cpanel_destination);
|
||||||
|
@unlink("{$cpanel_destination}.gz");
|
||||||
|
|
||||||
|
$cpanel_tar = new PharData($cpanel_destination);
|
||||||
|
|
||||||
|
$cpanel_tar->buildFromDirectory('./integrations/cpanel', "@integrations/cpanel/@");
|
||||||
|
|
||||||
|
$cpanel_path = '/usr/local/cpanel/base/3rdparty/snappymail/';
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$newFile = str_replace('\\', '/', $file);
|
||||||
|
$cpanel_tar->addFile($file, "{$cpanel_path}{$newFile}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cpanel_tar->addFile('.htaccess', "{$cpanel_path}.htaccess");
|
||||||
|
$cpanel_tar->addFile('README.md', "{$cpanel_path}README.md");
|
||||||
|
$cpanel_tar->addFromString("{$cpanel_path}VERSION", $package->version);
|
||||||
|
|
||||||
|
$index = file_get_contents('index.php');
|
||||||
|
$index = str_replace('0.0.0', $package->version, $index);
|
||||||
|
$cpanel_tar->addFromString("{$cpanel_path}index.php", $index);
|
||||||
|
|
||||||
|
$data = file_get_contents('dev/serviceworker.js');
|
||||||
|
$cpanel_tar->addFromString("{$cpanel_path}serviceworker.js", $data);
|
||||||
|
|
||||||
|
$cpanel_tar->compress(Phar::GZ);
|
||||||
|
unlink($cpanel_destination);
|
||||||
|
$cpanel_destination .= '.gz';
|
||||||
|
|
||||||
|
echo "{$cpanel_destination} created\n";
|
||||||
83
build/deb.php
Executable file
83
build/deb.php
Executable file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
echo "\x1b[33;1m === Debian === \x1b[0m\n";
|
||||||
|
|
||||||
|
// Debian Repository
|
||||||
|
define('DEB_SOURCE_DIR', __DIR__ . '/deb');
|
||||||
|
define('DEB_DEST_DIR', DEB_SOURCE_DIR . "/snappymail_{$package->version}-1_all");
|
||||||
|
is_dir(DEB_DEST_DIR) && passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
|
||||||
|
|
||||||
|
$dir = DEB_DEST_DIR . '/DEBIAN';
|
||||||
|
$data = file_get_contents(DEB_SOURCE_DIR . '/DEBIAN/control');
|
||||||
|
$data = str_replace('0.0.0', $package->version, $data);
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
file_put_contents("{$dir}/control", $data);
|
||||||
|
copy(DEB_SOURCE_DIR . '/DEBIAN/postinst', $dir . '/postinst');
|
||||||
|
chmod($dir . '/postinst', 0755);
|
||||||
|
|
||||||
|
$dir = DEB_DEST_DIR . '/var/lib/snappymail';
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
file_put_contents($dir . '/VERSION', $package->version);
|
||||||
|
copy('data/README.md', "{$dir}/README.md");
|
||||||
|
|
||||||
|
$dir = DEB_DEST_DIR . '/usr/share/doc/snappymail';
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
|
||||||
|
copy('CONTRIBUTING.md', "{$dir}/CONTRIBUTING.md");
|
||||||
|
copy('README.md', "{$dir}/README.md");
|
||||||
|
//copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
|
||||||
|
//usr/share/doc/snappymail/README.Debian
|
||||||
|
//usr/share/doc/snappymail/changelog.Debian.gz
|
||||||
|
//usr/share/doc/snappymail/copyright
|
||||||
|
|
||||||
|
// Move files into package directory
|
||||||
|
$dir = DEB_DEST_DIR . '/usr/share/snappymail';
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
passthru('cp -r "' . dirname(__DIR__) . '/snappymail" "' . $dir . '"');
|
||||||
|
|
||||||
|
rename("{$dir}/snappymail/v/0.0.0", "{$dir}/snappymail/v/{$package->version}");
|
||||||
|
|
||||||
|
$data = file_get_contents('index.php');
|
||||||
|
file_put_contents("{$dir}/index.php", str_replace('0.0.0', $package->version, $data));
|
||||||
|
|
||||||
|
$data = file_get_contents('_include.php');
|
||||||
|
file_put_contents("{$dir}/include.php", preg_replace('@(external-snappymail-data-folder/\'\);)@', "\$1\ndefine('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');", $data));
|
||||||
|
|
||||||
|
passthru('dpkg --build ' . escapeshellarg(DEB_DEST_DIR));
|
||||||
|
|
||||||
|
$TARGET_DIR = __DIR__ . "/dist/releases/webmail/{$package->version}/";
|
||||||
|
|
||||||
|
passthru('mv '
|
||||||
|
. escapeshellarg(DEB_DEST_DIR.'.deb') . ' '
|
||||||
|
. escapeshellarg($TARGET_DIR . basename(DEB_DEST_DIR.'.deb'))
|
||||||
|
);
|
||||||
|
|
||||||
|
passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
|
||||||
|
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
|
||||||
|
$cwd = getcwd();
|
||||||
|
chdir($TARGET_DIR);
|
||||||
|
passthru('dpkg-scanpackages . /dev/null > '.escapeshellarg($TARGET_DIR . 'Packages'));
|
||||||
|
passthru('dpkg-scanpackages . /dev/null | gzip -9c > '.escapeshellarg($TARGET_DIR . 'Packages.gz'));
|
||||||
|
$size = filesize($TARGET_DIR . 'Packages');
|
||||||
|
$gz_size = filesize($TARGET_DIR . 'Packages.gz');
|
||||||
|
$Release = 'Origin: SnappyMail Repository
|
||||||
|
Label: SnappyMail
|
||||||
|
Suite: stable
|
||||||
|
Codename: stable
|
||||||
|
Version: 1.0
|
||||||
|
Architectures: all
|
||||||
|
Components: main
|
||||||
|
Description: SnappyMail repository
|
||||||
|
Date: ' . gmdate('r') . '
|
||||||
|
MD5Sum:
|
||||||
|
' . hash_file('md5', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
||||||
|
' . hash_file('md5', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
||||||
|
SHA1:
|
||||||
|
' . hash_file('sha1', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
||||||
|
' . hash_file('sha1', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
||||||
|
SHA256:
|
||||||
|
' . hash_file('sha256', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
||||||
|
' . hash_file('sha256', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
||||||
|
';
|
||||||
|
file_put_contents($TARGET_DIR . 'Release', $Release);
|
||||||
|
chdir($cwd);
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
Package: snappymail
|
Package: snappymail
|
||||||
Version: 0.0.0
|
Version: 0.0.0
|
||||||
Maintainer: the-djmaze <debian@snappymail.eu>
|
Maintainer: SnappyMail <debian@snappymail.eu>
|
||||||
Depends:
|
Depends: nginx | apache2 | httpd, php-fpm | libapache2-mod-php, php-json, php-mbstring
|
||||||
nginx | apache2 | httpd
|
Recommends: php-intl, php-sodium, php-uuid
|
||||||
, php-fpm | libapache2-mod-php
|
Suggests: php-sqlite3 | php-mysql | php-pgsql, php-curl, php-exif, php-gnupg, php-gd | php-gmagick | php-imagick, php-openssl, php-zip
|
||||||
, php-json
|
|
||||||
, php-xml
|
|
||||||
, php-zip
|
|
||||||
Suggests: php-sqlite3 | php-mysql | php-pgsql
|
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Homepage: https://snappymail.eu
|
Homepage: https://snappymail.eu
|
||||||
Vcs-Browser: https://github.com/the-djmaze/snappymail
|
Vcs-Browser: https://github.com/the-djmaze/snappymail
|
||||||
|
|
|
||||||
3
build/deb/DEBIAN/postinst
Executable file
3
build/deb/DEBIAN/postinst
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
chown -R www-data:www-data /var/lib/snappymail
|
||||||
|
chown -R root:root /usr/share/snappymail/
|
||||||
92
build/nextcloud.php
Executable file
92
build/nextcloud.php
Executable file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
echo "\x1b[33;1m === Nextcloud === \x1b[0m\n";
|
||||||
|
|
||||||
|
$cert_dir = $_SERVER['HOME'].'/.nextcloud/certificates';
|
||||||
|
|
||||||
|
$nc_destination = "{$destPath}snappymail-{$package->version}-nextcloud.tar";
|
||||||
|
|
||||||
|
@unlink($nc_destination);
|
||||||
|
@unlink("{$nc_destination}.gz");
|
||||||
|
|
||||||
|
$nc_tar = new PharData($nc_destination);
|
||||||
|
$hashes = [];
|
||||||
|
|
||||||
|
file_put_contents(ROOT_DIR . '/integrations/nextcloud/snappymail/VERSION', $package->version);
|
||||||
|
$file = ROOT_DIR . '/integrations/nextcloud/snappymail/appinfo/info.xml';
|
||||||
|
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
|
||||||
|
|
||||||
|
$nc_tar->buildFromDirectory('./integrations/nextcloud', "@integrations/nextcloud/snappymail/@");
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/nextcloud/snappymail'));
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$name = str_replace('\\', '/', $file);
|
||||||
|
$name = str_replace('integrations/nextcloud/snappymail/', '', $name);
|
||||||
|
$hashes[$name] = hash_file('sha512', $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$newFile = str_replace('\\', '/', $file);
|
||||||
|
$newName = str_replace('/.htaccess', '/_htaccess', $newFile);
|
||||||
|
$nc_tar->addFile($file, "snappymail/app/{$newName}");
|
||||||
|
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
$nc_tar->addFile('data/.htaccess');
|
||||||
|
$nc_tar->addFromString('data/VERSION', $package->version);
|
||||||
|
$nc_tar->addFile('data/README.md');
|
||||||
|
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
|
||||||
|
*/
|
||||||
|
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
|
||||||
|
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
|
||||||
|
|
||||||
|
$index = file_get_contents('index.php');
|
||||||
|
$index = str_replace('0.0.0', $package->version, $index);
|
||||||
|
//$index = str_replace('snappymail/v/', '', $index);
|
||||||
|
$nc_tar->addFromString('snappymail/app/index.php', $index);
|
||||||
|
$hashes['app/index.php'] = hash('sha512', $index);
|
||||||
|
|
||||||
|
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
|
||||||
|
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
|
||||||
|
|
||||||
|
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
|
||||||
|
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
|
||||||
|
|
||||||
|
$data = file_get_contents('dev/serviceworker.js');
|
||||||
|
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
|
||||||
|
$hashes['app/serviceworker.js'] = hash('sha512', $data);
|
||||||
|
|
||||||
|
spl_autoload_register(function($name){
|
||||||
|
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
|
||||||
|
require $file;
|
||||||
|
});
|
||||||
|
|
||||||
|
ksort($hashes);
|
||||||
|
$cert = file_get_contents($cert_dir.'/snappymail.crt');
|
||||||
|
$rsa = new \phpseclib\Crypt\RSA();
|
||||||
|
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
|
||||||
|
$x509 = new \phpseclib\File\X509();
|
||||||
|
$x509->loadX509($cert);
|
||||||
|
$x509->setPrivateKey($rsa);
|
||||||
|
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
|
||||||
|
$rsa->setMGFHash('sha512');
|
||||||
|
$rsa->setSaltLength(0);
|
||||||
|
$signature = $rsa->sign(json_encode($hashes));
|
||||||
|
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
|
||||||
|
'hashes' => $hashes,
|
||||||
|
'signature' => base64_encode($signature),
|
||||||
|
'certificate' => $cert
|
||||||
|
], JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
|
$nc_tar->compress(Phar::GZ);
|
||||||
|
unlink($nc_destination);
|
||||||
|
$nc_destination .= '.gz';
|
||||||
|
|
||||||
|
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
|
||||||
|
file_put_contents($nc_destination.'.sig', $signature);
|
||||||
|
|
||||||
|
echo "{$nc_destination} created\n";
|
||||||
100
build/owncloud.php
Executable file
100
build/owncloud.php
Executable file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
echo "\x1b[33;1m === ownCloud === \x1b[0m\n";
|
||||||
|
|
||||||
|
$cert_dir = false; // $_SERVER['HOME'].'/.owncloud/certificates';
|
||||||
|
|
||||||
|
$nc_destination = "{$destPath}snappymail-{$package->version}-owncloud.tar";
|
||||||
|
|
||||||
|
@unlink($nc_destination);
|
||||||
|
@unlink("{$nc_destination}.gz");
|
||||||
|
|
||||||
|
$nc_tar = new PharData($nc_destination);
|
||||||
|
$hashes = [];
|
||||||
|
|
||||||
|
file_put_contents(ROOT_DIR . '/integrations/owncloud/snappymail/VERSION', $package->version);
|
||||||
|
$file = ROOT_DIR . '/integrations/owncloud/snappymail/appinfo/info.xml';
|
||||||
|
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
|
||||||
|
|
||||||
|
$nc_tar->buildFromDirectory('./integrations/owncloud', "@integrations/owncloud/snappymail/@");
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/owncloud/snappymail'));
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$name = str_replace('\\', '/', $file);
|
||||||
|
$name = str_replace('integrations/owncloud/snappymail/', '', $name);
|
||||||
|
$hashes[$name] = hash_file('sha512', $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$newFile = str_replace('\\', '/', $file);
|
||||||
|
// $newFile = str_replace("'snappymail/v/'.", '', $newFile);
|
||||||
|
$nc_tar->addFile($file, "snappymail/app/{$newFile}");
|
||||||
|
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/app/.htaccess");
|
||||||
|
$nc_tar->addFile("snappymail/v/{$package->version}/app/.htaccess", "snappymail/app/snappymail/v/{$package->version}/app/_htaccess");
|
||||||
|
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/static/.htaccess");
|
||||||
|
$nc_tar->addFile("snappymail/v/{$package->version}/static/.htaccess", "snappymail/app/snappymail/v/{$package->version}/static/_htaccess");
|
||||||
|
|
||||||
|
/*
|
||||||
|
$nc_tar->addFile('data/.htaccess');
|
||||||
|
$nc_tar->addFromString('data/VERSION', $package->version);
|
||||||
|
$nc_tar->addFile('data/README.md');
|
||||||
|
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
|
||||||
|
*/
|
||||||
|
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
|
||||||
|
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
|
||||||
|
|
||||||
|
$index = file_get_contents('index.php');
|
||||||
|
$index = str_replace('0.0.0', $package->version, $index);
|
||||||
|
//$index = str_replace('snappymail/v/', '', $index);
|
||||||
|
$nc_tar->addFromString('snappymail/app/index.php', $index);
|
||||||
|
$hashes['app/index.php'] = hash('sha512', $index);
|
||||||
|
|
||||||
|
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
|
||||||
|
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
|
||||||
|
|
||||||
|
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
|
||||||
|
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
|
||||||
|
|
||||||
|
$data = file_get_contents('dev/serviceworker.js');
|
||||||
|
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
|
||||||
|
$hashes['app/serviceworker.js'] = hash('sha512', $data);
|
||||||
|
|
||||||
|
if ($cert_dir) {
|
||||||
|
spl_autoload_register(function($name){
|
||||||
|
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
|
||||||
|
require $file;
|
||||||
|
});
|
||||||
|
|
||||||
|
ksort($hashes);
|
||||||
|
$cert = file_get_contents($cert_dir.'/snappymail.crt');
|
||||||
|
$rsa = new \phpseclib\Crypt\RSA();
|
||||||
|
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
|
||||||
|
$x509 = new \phpseclib\File\X509();
|
||||||
|
$x509->loadX509($cert);
|
||||||
|
$x509->setPrivateKey($rsa);
|
||||||
|
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
|
||||||
|
$rsa->setMGFHash('sha512');
|
||||||
|
$rsa->setSaltLength(0);
|
||||||
|
$signature = $rsa->sign(json_encode($hashes));
|
||||||
|
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
|
||||||
|
'hashes' => $hashes,
|
||||||
|
'signature' => base64_encode($signature),
|
||||||
|
'certificate' => $cert
|
||||||
|
], JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
$nc_tar->compress(Phar::GZ);
|
||||||
|
unlink($nc_destination);
|
||||||
|
$nc_destination .= '.gz';
|
||||||
|
|
||||||
|
if ($cert_dir) {
|
||||||
|
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
|
||||||
|
file_put_contents($nc_destination.'.sig', $signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "{$nc_destination} created\n";
|
||||||
893
build/phpseclib/Crypt/Hash.php
Normal file
893
build/phpseclib/Crypt/Hash.php
Normal file
|
|
@ -0,0 +1,893 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
|
||||||
|
*
|
||||||
|
* Uses hash() or mhash() if available and an internal implementation, otherwise. Currently supports the following:
|
||||||
|
*
|
||||||
|
* md2, md5, md5-96, sha1, sha1-96, sha256, sha256-96, sha384, and sha512, sha512-96
|
||||||
|
*
|
||||||
|
* If {@link self::setKey() setKey()} is called, {@link self::hash() hash()} will return the HMAC as opposed to
|
||||||
|
* the hash. If no valid algorithm is provided, sha1 will be used.
|
||||||
|
*
|
||||||
|
* PHP version 5
|
||||||
|
*
|
||||||
|
* {@internal The variable names are the same as those in
|
||||||
|
* {@link http://tools.ietf.org/html/rfc2104#section-2 RFC2104}.}}
|
||||||
|
*
|
||||||
|
* Here's a short example of how to use this library:
|
||||||
|
* <code>
|
||||||
|
* <?php
|
||||||
|
* include 'vendor/autoload.php';
|
||||||
|
*
|
||||||
|
* $hash = new \phpseclib\Crypt\Hash('sha1');
|
||||||
|
*
|
||||||
|
* $hash->setKey('abcdefg');
|
||||||
|
*
|
||||||
|
* echo base64_encode($hash->hash('abcdefg'));
|
||||||
|
* ?>
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @category Crypt
|
||||||
|
* @package Hash
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @copyright 2007 Jim Wigginton
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @link http://phpseclib.sourceforge.net
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace phpseclib\Crypt;
|
||||||
|
|
||||||
|
use phpseclib\Math\BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
|
||||||
|
*
|
||||||
|
* @package Hash
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
class Hash
|
||||||
|
{
|
||||||
|
/**#@+
|
||||||
|
* @access private
|
||||||
|
* @see \phpseclib\Crypt\Hash::__construct()
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Toggles the internal implementation
|
||||||
|
*/
|
||||||
|
const MODE_INTERNAL = 1;
|
||||||
|
/**
|
||||||
|
* Toggles the mhash() implementation, which has been deprecated on PHP 5.3.0+.
|
||||||
|
*/
|
||||||
|
const MODE_MHASH = 2;
|
||||||
|
/**
|
||||||
|
* Toggles the hash() implementation, which works on PHP 5.1.2+.
|
||||||
|
*/
|
||||||
|
const MODE_HASH = 3;
|
||||||
|
/**#@-*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Parameter
|
||||||
|
*
|
||||||
|
* @see self::setHash()
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $hashParam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte-length of compression blocks / key (Internal HMAC)
|
||||||
|
*
|
||||||
|
* @see self::setAlgorithm()
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $b;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte-length of hash output (Internal HMAC)
|
||||||
|
*
|
||||||
|
* @see self::setHash()
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $l = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Algorithm
|
||||||
|
*
|
||||||
|
* @see self::setHash()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $hash;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key
|
||||||
|
*
|
||||||
|
* @see self::setKey()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $key = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed Key
|
||||||
|
*
|
||||||
|
* @see self::_computeKey()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $computedKey = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outer XOR (Internal HMAC)
|
||||||
|
*
|
||||||
|
* @see self::setKey()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $opad;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inner XOR (Internal HMAC)
|
||||||
|
*
|
||||||
|
* @see self::setKey()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $ipad;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Engine
|
||||||
|
*
|
||||||
|
* @see self::setHash()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $engine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Constructor.
|
||||||
|
*
|
||||||
|
* @param string $hash
|
||||||
|
* @return \phpseclib\Crypt\Hash
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function __construct($hash = 'sha1')
|
||||||
|
{
|
||||||
|
if (!defined('CRYPT_HASH_MODE')) {
|
||||||
|
switch (true) {
|
||||||
|
case extension_loaded('hash'):
|
||||||
|
define('CRYPT_HASH_MODE', self::MODE_HASH);
|
||||||
|
break;
|
||||||
|
case extension_loaded('mhash'):
|
||||||
|
define('CRYPT_HASH_MODE', self::MODE_MHASH);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
define('CRYPT_HASH_MODE', self::MODE_INTERNAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setHash($hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the key for HMACs
|
||||||
|
*
|
||||||
|
* Keys can be of any length.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $key
|
||||||
|
*/
|
||||||
|
function setKey($key = false)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
$this->_computeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-compute the key used by the HMAC
|
||||||
|
*
|
||||||
|
* Quoting http://tools.ietf.org/html/rfc2104#section-2, "Applications that use keys longer than B bytes
|
||||||
|
* will first hash the key using H and then use the resultant L byte string as the actual key to HMAC."
|
||||||
|
*
|
||||||
|
* As documented in https://www.reddit.com/r/PHP/comments/9nct2l/symfonypolyfill_hash_pbkdf2_correct_fix_for/
|
||||||
|
* when doing an HMAC multiple times it's faster to compute the hash once instead of computing it during
|
||||||
|
* every call
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
function _computeKey()
|
||||||
|
{
|
||||||
|
if ($this->key === false) {
|
||||||
|
$this->computedKey = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($this->key) <= $this->b) {
|
||||||
|
$this->computedKey = $this->key;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->engine) {
|
||||||
|
case self::MODE_MHASH:
|
||||||
|
$this->computedKey = mhash($this->hash, $this->key);
|
||||||
|
break;
|
||||||
|
case self::MODE_HASH:
|
||||||
|
$this->computedKey = hash($this->hash, $this->key, true);
|
||||||
|
break;
|
||||||
|
case self::MODE_INTERNAL:
|
||||||
|
$this->computedKey = call_user_func($this->hash, $this->key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the hash function.
|
||||||
|
*
|
||||||
|
* As set by the constructor or by the setHash() method.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getHash()
|
||||||
|
{
|
||||||
|
return $this->hashParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the hash function.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $hash
|
||||||
|
*/
|
||||||
|
function setHash($hash)
|
||||||
|
{
|
||||||
|
$this->hashParam = $hash = strtolower($hash);
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md5-96':
|
||||||
|
case 'sha1-96':
|
||||||
|
case 'sha256-96':
|
||||||
|
case 'sha512-96':
|
||||||
|
$hash = substr($hash, 0, -3);
|
||||||
|
$this->l = 12; // 96 / 8 = 12
|
||||||
|
break;
|
||||||
|
case 'md2':
|
||||||
|
case 'md5':
|
||||||
|
$this->l = 16;
|
||||||
|
break;
|
||||||
|
case 'sha1':
|
||||||
|
$this->l = 20;
|
||||||
|
break;
|
||||||
|
case 'sha256':
|
||||||
|
$this->l = 32;
|
||||||
|
break;
|
||||||
|
case 'sha384':
|
||||||
|
$this->l = 48;
|
||||||
|
break;
|
||||||
|
case 'sha512':
|
||||||
|
$this->l = 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md2-96':
|
||||||
|
case 'md2':
|
||||||
|
$this->b = 16;
|
||||||
|
case 'md5-96':
|
||||||
|
case 'sha1-96':
|
||||||
|
case 'sha224-96':
|
||||||
|
case 'sha256-96':
|
||||||
|
case 'md2':
|
||||||
|
case 'md5':
|
||||||
|
case 'sha1':
|
||||||
|
case 'sha224':
|
||||||
|
case 'sha256':
|
||||||
|
$this->b = 64;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->b = 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md2':
|
||||||
|
$this->engine = CRYPT_HASH_MODE == self::MODE_HASH && in_array('md2', hash_algos()) ?
|
||||||
|
self::MODE_HASH : self::MODE_INTERNAL;
|
||||||
|
break;
|
||||||
|
case 'sha384':
|
||||||
|
case 'sha512':
|
||||||
|
$this->engine = CRYPT_HASH_MODE == self::MODE_MHASH ? self::MODE_INTERNAL : CRYPT_HASH_MODE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->engine = CRYPT_HASH_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->engine) {
|
||||||
|
case self::MODE_MHASH:
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md5':
|
||||||
|
$this->hash = MHASH_MD5;
|
||||||
|
break;
|
||||||
|
case 'sha256':
|
||||||
|
$this->hash = MHASH_SHA256;
|
||||||
|
break;
|
||||||
|
case 'sha1':
|
||||||
|
default:
|
||||||
|
$this->hash = MHASH_SHA1;
|
||||||
|
}
|
||||||
|
$this->_computeKey(self::MODE_MHASH);
|
||||||
|
return;
|
||||||
|
case self::MODE_HASH:
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md5':
|
||||||
|
$this->hash = 'md5';
|
||||||
|
return;
|
||||||
|
case 'md2':
|
||||||
|
case 'sha256':
|
||||||
|
case 'sha384':
|
||||||
|
case 'sha512':
|
||||||
|
$this->hash = $hash;
|
||||||
|
return;
|
||||||
|
case 'sha1':
|
||||||
|
default:
|
||||||
|
$this->hash = 'sha1';
|
||||||
|
}
|
||||||
|
$this->_computeKey(self::MODE_HASH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md2':
|
||||||
|
$this->hash = array($this, '_md2');
|
||||||
|
break;
|
||||||
|
case 'md5':
|
||||||
|
$this->hash = array($this, '_md5');
|
||||||
|
break;
|
||||||
|
case 'sha256':
|
||||||
|
$this->hash = array($this, '_sha256');
|
||||||
|
break;
|
||||||
|
case 'sha384':
|
||||||
|
case 'sha512':
|
||||||
|
$this->hash = array($this, '_sha512');
|
||||||
|
break;
|
||||||
|
case 'sha1':
|
||||||
|
default:
|
||||||
|
$this->hash = array($this, '_sha1');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ipad = str_repeat(chr(0x36), $this->b);
|
||||||
|
$this->opad = str_repeat(chr(0x5C), $this->b);
|
||||||
|
|
||||||
|
$this->_computeKey(self::MODE_INTERNAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the HMAC.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $text
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function hash($text)
|
||||||
|
{
|
||||||
|
if (!empty($this->key) || is_string($this->key)) {
|
||||||
|
switch ($this->engine) {
|
||||||
|
case self::MODE_MHASH:
|
||||||
|
$output = mhash($this->hash, $text, $this->computedKey);
|
||||||
|
break;
|
||||||
|
case self::MODE_HASH:
|
||||||
|
$output = hash_hmac($this->hash, $text, $this->computedKey, true);
|
||||||
|
break;
|
||||||
|
case self::MODE_INTERNAL:
|
||||||
|
$key = str_pad($this->computedKey, $this->b, chr(0)); // step 1
|
||||||
|
$temp = $this->ipad ^ $key; // step 2
|
||||||
|
$temp .= $text; // step 3
|
||||||
|
$temp = call_user_func($this->hash, $temp); // step 4
|
||||||
|
$output = $this->opad ^ $key; // step 5
|
||||||
|
$output.= $temp; // step 6
|
||||||
|
$output = call_user_func($this->hash, $output); // step 7
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch ($this->engine) {
|
||||||
|
case self::MODE_MHASH:
|
||||||
|
$output = mhash($this->hash, $text);
|
||||||
|
break;
|
||||||
|
case self::MODE_HASH:
|
||||||
|
$output = hash($this->hash, $text, true);
|
||||||
|
break;
|
||||||
|
case self::MODE_INTERNAL:
|
||||||
|
$output = call_user_func($this->hash, $text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($output, 0, $this->l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hash length (in bytes)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function getLength()
|
||||||
|
{
|
||||||
|
return $this->l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for MD5
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _md5($m)
|
||||||
|
{
|
||||||
|
return pack('H*', md5($m));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for SHA1
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _sha1($m)
|
||||||
|
{
|
||||||
|
return pack('H*', sha1($m));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementation of MD2
|
||||||
|
*
|
||||||
|
* See {@link http://tools.ietf.org/html/rfc1319 RFC1319}.
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _md2($m)
|
||||||
|
{
|
||||||
|
static $s = array(
|
||||||
|
41, 46, 67, 201, 162, 216, 124, 1, 61, 54, 84, 161, 236, 240, 6,
|
||||||
|
19, 98, 167, 5, 243, 192, 199, 115, 140, 152, 147, 43, 217, 188,
|
||||||
|
76, 130, 202, 30, 155, 87, 60, 253, 212, 224, 22, 103, 66, 111, 24,
|
||||||
|
138, 23, 229, 18, 190, 78, 196, 214, 218, 158, 222, 73, 160, 251,
|
||||||
|
245, 142, 187, 47, 238, 122, 169, 104, 121, 145, 21, 178, 7, 63,
|
||||||
|
148, 194, 16, 137, 11, 34, 95, 33, 128, 127, 93, 154, 90, 144, 50,
|
||||||
|
39, 53, 62, 204, 231, 191, 247, 151, 3, 255, 25, 48, 179, 72, 165,
|
||||||
|
181, 209, 215, 94, 146, 42, 172, 86, 170, 198, 79, 184, 56, 210,
|
||||||
|
150, 164, 125, 182, 118, 252, 107, 226, 156, 116, 4, 241, 69, 157,
|
||||||
|
112, 89, 100, 113, 135, 32, 134, 91, 207, 101, 230, 45, 168, 2, 27,
|
||||||
|
96, 37, 173, 174, 176, 185, 246, 28, 70, 97, 105, 52, 64, 126, 15,
|
||||||
|
85, 71, 163, 35, 221, 81, 175, 58, 195, 92, 249, 206, 186, 197,
|
||||||
|
234, 38, 44, 83, 13, 110, 133, 40, 132, 9, 211, 223, 205, 244, 65,
|
||||||
|
129, 77, 82, 106, 220, 55, 200, 108, 193, 171, 250, 36, 225, 123,
|
||||||
|
8, 12, 189, 177, 74, 120, 136, 149, 139, 227, 99, 232, 109, 233,
|
||||||
|
203, 213, 254, 59, 0, 29, 57, 242, 239, 183, 14, 102, 88, 208, 228,
|
||||||
|
166, 119, 114, 248, 235, 117, 75, 10, 49, 68, 80, 180, 143, 237,
|
||||||
|
31, 26, 219, 153, 141, 51, 159, 17, 131, 20
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 1. Append Padding Bytes
|
||||||
|
$pad = 16 - (strlen($m) & 0xF);
|
||||||
|
$m.= str_repeat(chr($pad), $pad);
|
||||||
|
|
||||||
|
$length = strlen($m);
|
||||||
|
|
||||||
|
// Step 2. Append Checksum
|
||||||
|
$c = str_repeat(chr(0), 16);
|
||||||
|
$l = chr(0);
|
||||||
|
for ($i = 0; $i < $length; $i+= 16) {
|
||||||
|
for ($j = 0; $j < 16; $j++) {
|
||||||
|
// RFC1319 incorrectly states that C[j] should be set to S[c xor L]
|
||||||
|
//$c[$j] = chr($s[ord($m[$i + $j] ^ $l)]);
|
||||||
|
// per <http://www.rfc-editor.org/errata_search.php?rfc=1319>, however, C[j] should be set to S[c xor L] xor C[j]
|
||||||
|
$c[$j] = chr($s[ord($m[$i + $j] ^ $l)] ^ ord($c[$j]));
|
||||||
|
$l = $c[$j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$m.= $c;
|
||||||
|
|
||||||
|
$length+= 16;
|
||||||
|
|
||||||
|
// Step 3. Initialize MD Buffer
|
||||||
|
$x = str_repeat(chr(0), 48);
|
||||||
|
|
||||||
|
// Step 4. Process Message in 16-Byte Blocks
|
||||||
|
for ($i = 0; $i < $length; $i+= 16) {
|
||||||
|
for ($j = 0; $j < 16; $j++) {
|
||||||
|
$x[$j + 16] = $m[$i + $j];
|
||||||
|
$x[$j + 32] = $x[$j + 16] ^ $x[$j];
|
||||||
|
}
|
||||||
|
$t = chr(0);
|
||||||
|
for ($j = 0; $j < 18; $j++) {
|
||||||
|
for ($k = 0; $k < 48; $k++) {
|
||||||
|
$x[$k] = $t = $x[$k] ^ chr($s[ord($t)]);
|
||||||
|
//$t = $x[$k] = $x[$k] ^ chr($s[ord($t)]);
|
||||||
|
}
|
||||||
|
$t = chr(ord($t) + $j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5. Output
|
||||||
|
return substr($x, 0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementation of SHA256
|
||||||
|
*
|
||||||
|
* See {@link http://en.wikipedia.org/wiki/SHA_hash_functions#SHA-256_.28a_SHA-2_variant.29_pseudocode SHA-256 (a SHA-2 variant) pseudocode - Wikipedia}.
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _sha256($m)
|
||||||
|
{
|
||||||
|
if (extension_loaded('suhosin')) {
|
||||||
|
return pack('H*', sha256($m));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize variables
|
||||||
|
$hash = array(
|
||||||
|
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
||||||
|
);
|
||||||
|
// Initialize table of round constants
|
||||||
|
// (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311)
|
||||||
|
static $k = array(
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pre-processing
|
||||||
|
$length = strlen($m);
|
||||||
|
// to round to nearest 56 mod 64, we'll add 64 - (length + (64 - 56)) % 64
|
||||||
|
$m.= str_repeat(chr(0), 64 - (($length + 8) & 0x3F));
|
||||||
|
$m[$length] = chr(0x80);
|
||||||
|
// we don't support hashing strings 512MB long
|
||||||
|
$m.= pack('N2', 0, $length << 3);
|
||||||
|
|
||||||
|
// Process the message in successive 512-bit chunks
|
||||||
|
$chunks = str_split($m, 64);
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
$w = array();
|
||||||
|
for ($i = 0; $i < 16; $i++) {
|
||||||
|
extract(unpack('Ntemp', $this->_string_shift($chunk, 4)));
|
||||||
|
$w[] = $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend the sixteen 32-bit words into sixty-four 32-bit words
|
||||||
|
for ($i = 16; $i < 64; $i++) {
|
||||||
|
// @codingStandardsIgnoreStart
|
||||||
|
$s0 = $this->_rightRotate($w[$i - 15], 7) ^
|
||||||
|
$this->_rightRotate($w[$i - 15], 18) ^
|
||||||
|
$this->_rightShift( $w[$i - 15], 3);
|
||||||
|
$s1 = $this->_rightRotate($w[$i - 2], 17) ^
|
||||||
|
$this->_rightRotate($w[$i - 2], 19) ^
|
||||||
|
$this->_rightShift( $w[$i - 2], 10);
|
||||||
|
// @codingStandardsIgnoreEnd
|
||||||
|
$w[$i] = $this->_add($w[$i - 16], $s0, $w[$i - 7], $s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize hash value for this chunk
|
||||||
|
list($a, $b, $c, $d, $e, $f, $g, $h) = $hash;
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
for ($i = 0; $i < 64; $i++) {
|
||||||
|
$s0 = $this->_rightRotate($a, 2) ^
|
||||||
|
$this->_rightRotate($a, 13) ^
|
||||||
|
$this->_rightRotate($a, 22);
|
||||||
|
$maj = ($a & $b) ^
|
||||||
|
($a & $c) ^
|
||||||
|
($b & $c);
|
||||||
|
$t2 = $this->_add($s0, $maj);
|
||||||
|
|
||||||
|
$s1 = $this->_rightRotate($e, 6) ^
|
||||||
|
$this->_rightRotate($e, 11) ^
|
||||||
|
$this->_rightRotate($e, 25);
|
||||||
|
$ch = ($e & $f) ^
|
||||||
|
($this->_not($e) & $g);
|
||||||
|
$t1 = $this->_add($h, $s1, $ch, $k[$i], $w[$i]);
|
||||||
|
|
||||||
|
$h = $g;
|
||||||
|
$g = $f;
|
||||||
|
$f = $e;
|
||||||
|
$e = $this->_add($d, $t1);
|
||||||
|
$d = $c;
|
||||||
|
$c = $b;
|
||||||
|
$b = $a;
|
||||||
|
$a = $this->_add($t1, $t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this chunk's hash to result so far
|
||||||
|
$hash = array(
|
||||||
|
$this->_add($hash[0], $a),
|
||||||
|
$this->_add($hash[1], $b),
|
||||||
|
$this->_add($hash[2], $c),
|
||||||
|
$this->_add($hash[3], $d),
|
||||||
|
$this->_add($hash[4], $e),
|
||||||
|
$this->_add($hash[5], $f),
|
||||||
|
$this->_add($hash[6], $g),
|
||||||
|
$this->_add($hash[7], $h)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce the final hash value (big-endian)
|
||||||
|
return pack('N8', $hash[0], $hash[1], $hash[2], $hash[3], $hash[4], $hash[5], $hash[6], $hash[7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementation of SHA384 and SHA512
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _sha512($m)
|
||||||
|
{
|
||||||
|
static $init384, $init512, $k;
|
||||||
|
|
||||||
|
if (!isset($k)) {
|
||||||
|
// Initialize variables
|
||||||
|
$init384 = array( // initial values for SHA384
|
||||||
|
'cbbb9d5dc1059ed8', '629a292a367cd507', '9159015a3070dd17', '152fecd8f70e5939',
|
||||||
|
'67332667ffc00b31', '8eb44a8768581511', 'db0c2e0d64f98fa7', '47b5481dbefa4fa4'
|
||||||
|
);
|
||||||
|
$init512 = array( // initial values for SHA512
|
||||||
|
'6a09e667f3bcc908', 'bb67ae8584caa73b', '3c6ef372fe94f82b', 'a54ff53a5f1d36f1',
|
||||||
|
'510e527fade682d1', '9b05688c2b3e6c1f', '1f83d9abfb41bd6b', '5be0cd19137e2179'
|
||||||
|
);
|
||||||
|
|
||||||
|
for ($i = 0; $i < 8; $i++) {
|
||||||
|
$init384[$i] = new BigInteger($init384[$i], 16);
|
||||||
|
$init384[$i]->setPrecision(64);
|
||||||
|
$init512[$i] = new BigInteger($init512[$i], 16);
|
||||||
|
$init512[$i]->setPrecision(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize table of round constants
|
||||||
|
// (first 64 bits of the fractional parts of the cube roots of the first 80 primes 2..409)
|
||||||
|
$k = array(
|
||||||
|
'428a2f98d728ae22', '7137449123ef65cd', 'b5c0fbcfec4d3b2f', 'e9b5dba58189dbbc',
|
||||||
|
'3956c25bf348b538', '59f111f1b605d019', '923f82a4af194f9b', 'ab1c5ed5da6d8118',
|
||||||
|
'd807aa98a3030242', '12835b0145706fbe', '243185be4ee4b28c', '550c7dc3d5ffb4e2',
|
||||||
|
'72be5d74f27b896f', '80deb1fe3b1696b1', '9bdc06a725c71235', 'c19bf174cf692694',
|
||||||
|
'e49b69c19ef14ad2', 'efbe4786384f25e3', '0fc19dc68b8cd5b5', '240ca1cc77ac9c65',
|
||||||
|
'2de92c6f592b0275', '4a7484aa6ea6e483', '5cb0a9dcbd41fbd4', '76f988da831153b5',
|
||||||
|
'983e5152ee66dfab', 'a831c66d2db43210', 'b00327c898fb213f', 'bf597fc7beef0ee4',
|
||||||
|
'c6e00bf33da88fc2', 'd5a79147930aa725', '06ca6351e003826f', '142929670a0e6e70',
|
||||||
|
'27b70a8546d22ffc', '2e1b21385c26c926', '4d2c6dfc5ac42aed', '53380d139d95b3df',
|
||||||
|
'650a73548baf63de', '766a0abb3c77b2a8', '81c2c92e47edaee6', '92722c851482353b',
|
||||||
|
'a2bfe8a14cf10364', 'a81a664bbc423001', 'c24b8b70d0f89791', 'c76c51a30654be30',
|
||||||
|
'd192e819d6ef5218', 'd69906245565a910', 'f40e35855771202a', '106aa07032bbd1b8',
|
||||||
|
'19a4c116b8d2d0c8', '1e376c085141ab53', '2748774cdf8eeb99', '34b0bcb5e19b48a8',
|
||||||
|
'391c0cb3c5c95a63', '4ed8aa4ae3418acb', '5b9cca4f7763e373', '682e6ff3d6b2b8a3',
|
||||||
|
'748f82ee5defb2fc', '78a5636f43172f60', '84c87814a1f0ab72', '8cc702081a6439ec',
|
||||||
|
'90befffa23631e28', 'a4506cebde82bde9', 'bef9a3f7b2c67915', 'c67178f2e372532b',
|
||||||
|
'ca273eceea26619c', 'd186b8c721c0c207', 'eada7dd6cde0eb1e', 'f57d4f7fee6ed178',
|
||||||
|
'06f067aa72176fba', '0a637dc5a2c898a6', '113f9804bef90dae', '1b710b35131c471b',
|
||||||
|
'28db77f523047d84', '32caab7b40c72493', '3c9ebe0a15c9bebc', '431d67c49c100d4c',
|
||||||
|
'4cc5d4becb3e42b6', '597f299cfc657e2a', '5fcb6fab3ad6faec', '6c44198c4a475817'
|
||||||
|
);
|
||||||
|
|
||||||
|
for ($i = 0; $i < 80; $i++) {
|
||||||
|
$k[$i] = new BigInteger($k[$i], 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = $this->l == 48 ? $init384 : $init512;
|
||||||
|
|
||||||
|
// Pre-processing
|
||||||
|
$length = strlen($m);
|
||||||
|
// to round to nearest 112 mod 128, we'll add 128 - (length + (128 - 112)) % 128
|
||||||
|
$m.= str_repeat(chr(0), 128 - (($length + 16) & 0x7F));
|
||||||
|
$m[$length] = chr(0x80);
|
||||||
|
// we don't support hashing strings 512MB long
|
||||||
|
$m.= pack('N4', 0, 0, 0, $length << 3);
|
||||||
|
|
||||||
|
// Process the message in successive 1024-bit chunks
|
||||||
|
$chunks = str_split($m, 128);
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
$w = array();
|
||||||
|
for ($i = 0; $i < 16; $i++) {
|
||||||
|
$temp = new BigInteger($this->_string_shift($chunk, 8), 256);
|
||||||
|
$temp->setPrecision(64);
|
||||||
|
$w[] = $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend the sixteen 32-bit words into eighty 32-bit words
|
||||||
|
for ($i = 16; $i < 80; $i++) {
|
||||||
|
$temp = array(
|
||||||
|
$w[$i - 15]->bitwise_rightRotate(1),
|
||||||
|
$w[$i - 15]->bitwise_rightRotate(8),
|
||||||
|
$w[$i - 15]->bitwise_rightShift(7)
|
||||||
|
);
|
||||||
|
$s0 = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$s0 = $s0->bitwise_xor($temp[2]);
|
||||||
|
$temp = array(
|
||||||
|
$w[$i - 2]->bitwise_rightRotate(19),
|
||||||
|
$w[$i - 2]->bitwise_rightRotate(61),
|
||||||
|
$w[$i - 2]->bitwise_rightShift(6)
|
||||||
|
);
|
||||||
|
$s1 = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$s1 = $s1->bitwise_xor($temp[2]);
|
||||||
|
$w[$i] = $w[$i - 16]->copy();
|
||||||
|
$w[$i] = $w[$i]->add($s0);
|
||||||
|
$w[$i] = $w[$i]->add($w[$i - 7]);
|
||||||
|
$w[$i] = $w[$i]->add($s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize hash value for this chunk
|
||||||
|
$a = $hash[0]->copy();
|
||||||
|
$b = $hash[1]->copy();
|
||||||
|
$c = $hash[2]->copy();
|
||||||
|
$d = $hash[3]->copy();
|
||||||
|
$e = $hash[4]->copy();
|
||||||
|
$f = $hash[5]->copy();
|
||||||
|
$g = $hash[6]->copy();
|
||||||
|
$h = $hash[7]->copy();
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
for ($i = 0; $i < 80; $i++) {
|
||||||
|
$temp = array(
|
||||||
|
$a->bitwise_rightRotate(28),
|
||||||
|
$a->bitwise_rightRotate(34),
|
||||||
|
$a->bitwise_rightRotate(39)
|
||||||
|
);
|
||||||
|
$s0 = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$s0 = $s0->bitwise_xor($temp[2]);
|
||||||
|
$temp = array(
|
||||||
|
$a->bitwise_and($b),
|
||||||
|
$a->bitwise_and($c),
|
||||||
|
$b->bitwise_and($c)
|
||||||
|
);
|
||||||
|
$maj = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$maj = $maj->bitwise_xor($temp[2]);
|
||||||
|
$t2 = $s0->add($maj);
|
||||||
|
|
||||||
|
$temp = array(
|
||||||
|
$e->bitwise_rightRotate(14),
|
||||||
|
$e->bitwise_rightRotate(18),
|
||||||
|
$e->bitwise_rightRotate(41)
|
||||||
|
);
|
||||||
|
$s1 = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$s1 = $s1->bitwise_xor($temp[2]);
|
||||||
|
$temp = array(
|
||||||
|
$e->bitwise_and($f),
|
||||||
|
$g->bitwise_and($e->bitwise_not())
|
||||||
|
);
|
||||||
|
$ch = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$t1 = $h->add($s1);
|
||||||
|
$t1 = $t1->add($ch);
|
||||||
|
$t1 = $t1->add($k[$i]);
|
||||||
|
$t1 = $t1->add($w[$i]);
|
||||||
|
|
||||||
|
$h = $g->copy();
|
||||||
|
$g = $f->copy();
|
||||||
|
$f = $e->copy();
|
||||||
|
$e = $d->add($t1);
|
||||||
|
$d = $c->copy();
|
||||||
|
$c = $b->copy();
|
||||||
|
$b = $a->copy();
|
||||||
|
$a = $t1->add($t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this chunk's hash to result so far
|
||||||
|
$hash = array(
|
||||||
|
$hash[0]->add($a),
|
||||||
|
$hash[1]->add($b),
|
||||||
|
$hash[2]->add($c),
|
||||||
|
$hash[3]->add($d),
|
||||||
|
$hash[4]->add($e),
|
||||||
|
$hash[5]->add($f),
|
||||||
|
$hash[6]->add($g),
|
||||||
|
$hash[7]->add($h)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce the final hash value (big-endian)
|
||||||
|
// (\phpseclib\Crypt\Hash::hash() trims the output for hashes but not for HMACs. as such, we trim the output here)
|
||||||
|
$temp = $hash[0]->toBytes() . $hash[1]->toBytes() . $hash[2]->toBytes() . $hash[3]->toBytes() .
|
||||||
|
$hash[4]->toBytes() . $hash[5]->toBytes();
|
||||||
|
if ($this->l != 48) {
|
||||||
|
$temp.= $hash[6]->toBytes() . $hash[7]->toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Right Rotate
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param int $int
|
||||||
|
* @param int $amt
|
||||||
|
* @see self::_sha256()
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function _rightRotate($int, $amt)
|
||||||
|
{
|
||||||
|
$invamt = 32 - $amt;
|
||||||
|
$mask = (1 << $invamt) - 1;
|
||||||
|
return (($int << $invamt) & 0xFFFFFFFF) | (($int >> $amt) & $mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Right Shift
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param int $int
|
||||||
|
* @param int $amt
|
||||||
|
* @see self::_sha256()
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function _rightShift($int, $amt)
|
||||||
|
{
|
||||||
|
$mask = (1 << (32 - $amt)) - 1;
|
||||||
|
return ($int >> $amt) & $mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param int $int
|
||||||
|
* @see self::_sha256()
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function _not($int)
|
||||||
|
{
|
||||||
|
return ~$int & 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add
|
||||||
|
*
|
||||||
|
* _sha256() adds multiple unsigned 32-bit integers. Since PHP doesn't support unsigned integers and since the
|
||||||
|
* possibility of overflow exists, care has to be taken. BigInteger could be used but this should be faster.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
* @see self::_sha256()
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
function _add()
|
||||||
|
{
|
||||||
|
static $mod;
|
||||||
|
if (!isset($mod)) {
|
||||||
|
$mod = pow(2, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = 0;
|
||||||
|
$arguments = func_get_args();
|
||||||
|
foreach ($arguments as $argument) {
|
||||||
|
$result+= $argument < 0 ? ($argument & 0x7FFFFFFF) + 0x80000000 : $argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((php_uname('m') & "\xDF\xDF\xDF") != 'ARM') {
|
||||||
|
return fmod($result, $mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (fmod($result, 0x80000000) & 0x7FFFFFFF) |
|
||||||
|
((fmod(floor($result / 0x80000000), 2) & 1) << 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String Shift
|
||||||
|
*
|
||||||
|
* Inspired by array_shift
|
||||||
|
*
|
||||||
|
* @param string $string
|
||||||
|
* @param int $index
|
||||||
|
* @return string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
function _string_shift(&$string, $index = 1)
|
||||||
|
{
|
||||||
|
$substr = substr($string, 0, $index);
|
||||||
|
$string = substr($string, $index);
|
||||||
|
return $substr;
|
||||||
|
}
|
||||||
|
}
|
||||||
3265
build/phpseclib/Crypt/RSA.php
Normal file
3265
build/phpseclib/Crypt/RSA.php
Normal file
File diff suppressed because it is too large
Load diff
277
build/phpseclib/Crypt/Random.php
Normal file
277
build/phpseclib/Crypt/Random.php
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Random Number Generator
|
||||||
|
*
|
||||||
|
* PHP version 5
|
||||||
|
*
|
||||||
|
* Here's a short example of how to use this library:
|
||||||
|
* <code>
|
||||||
|
* <?php
|
||||||
|
* include 'vendor/autoload.php';
|
||||||
|
*
|
||||||
|
* echo bin2hex(\phpseclib\Crypt\Random::string(8));
|
||||||
|
* ?>
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @category Crypt
|
||||||
|
* @package Random
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @copyright 2007 Jim Wigginton
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @link http://phpseclib.sourceforge.net
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace phpseclib\Crypt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP Random Number Generator
|
||||||
|
*
|
||||||
|
* @package Random
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
class Random
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate a random string.
|
||||||
|
*
|
||||||
|
* Although microoptimizations are generally discouraged as they impair readability this function is ripe with
|
||||||
|
* microoptimizations because this function has the potential of being called a huge number of times.
|
||||||
|
* eg. for RSA key generation.
|
||||||
|
*
|
||||||
|
* @param int $length
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
static function string($length)
|
||||||
|
{
|
||||||
|
if (!$length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
|
||||||
|
try {
|
||||||
|
return \random_bytes($length);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// If a sufficient source of randomness is unavailable, random_bytes() will throw an
|
||||||
|
// object that implements the Throwable interface (Exception, TypeError, Error).
|
||||||
|
// We don't actually need to do anything here. The string() method should just continue
|
||||||
|
// as normal. Note, however, that if we don't have a sufficient source of randomness for
|
||||||
|
// random_bytes(), most of the other calls here will fail too, so we'll end up using
|
||||||
|
// the PHP implementation.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
// method 1. prior to PHP 5.3 this would call rand() on windows hence the function_exists('class_alias') call.
|
||||||
|
// ie. class_alias is a function that was introduced in PHP 5.3
|
||||||
|
if (extension_loaded('mcrypt') && function_exists('class_alias')) {
|
||||||
|
return @mcrypt_create_iv($length);
|
||||||
|
}
|
||||||
|
// method 2. openssl_random_pseudo_bytes was introduced in PHP 5.3.0 but prior to PHP 5.3.4 there was,
|
||||||
|
// to quote <http://php.net/ChangeLog-5.php#5.3.4>, "possible blocking behavior". as of 5.3.4
|
||||||
|
// openssl_random_pseudo_bytes and mcrypt_create_iv do the exact same thing on Windows. ie. they both
|
||||||
|
// call php_win32_get_random_bytes():
|
||||||
|
//
|
||||||
|
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/openssl/openssl.c#L5008
|
||||||
|
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1392
|
||||||
|
//
|
||||||
|
// php_win32_get_random_bytes() is defined thusly:
|
||||||
|
//
|
||||||
|
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/win32/winutil.c#L80
|
||||||
|
//
|
||||||
|
// we're calling it, all the same, in the off chance that the mcrypt extension is not available
|
||||||
|
if (extension_loaded('openssl') && version_compare(PHP_VERSION, '5.3.4', '>=')) {
|
||||||
|
return openssl_random_pseudo_bytes($length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// method 1. the fastest
|
||||||
|
if (extension_loaded('openssl')) {
|
||||||
|
return openssl_random_pseudo_bytes($length);
|
||||||
|
}
|
||||||
|
// method 2
|
||||||
|
static $fp = true;
|
||||||
|
if ($fp === true) {
|
||||||
|
// warning's will be output unles the error suppression operator is used. errors such as
|
||||||
|
// "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc.
|
||||||
|
$fp = @fopen('/dev/urandom', 'rb');
|
||||||
|
}
|
||||||
|
if ($fp !== true && $fp !== false) { // surprisingly faster than !is_bool() or is_resource()
|
||||||
|
$temp = fread($fp, $length);
|
||||||
|
if (strlen($temp) == $length) {
|
||||||
|
return $temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// method 3. pretty much does the same thing as method 2 per the following url:
|
||||||
|
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1391
|
||||||
|
// surprisingly slower than method 2. maybe that's because mcrypt_create_iv does a bunch of error checking that we're
|
||||||
|
// not doing. regardless, this'll only be called if this PHP script couldn't open /dev/urandom due to open_basedir
|
||||||
|
// restrictions or some such
|
||||||
|
if (extension_loaded('mcrypt')) {
|
||||||
|
return @mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// at this point we have no choice but to use a pure-PHP CSPRNG
|
||||||
|
|
||||||
|
// cascade entropy across multiple PHP instances by fixing the session and collecting all
|
||||||
|
// environmental variables, including the previous session data and the current session
|
||||||
|
// data.
|
||||||
|
//
|
||||||
|
// mt_rand seeds itself by looking at the PID and the time, both of which are (relatively)
|
||||||
|
// easy to guess at. linux uses mouse clicks, keyboard timings, etc, as entropy sources, but
|
||||||
|
// PHP isn't low level to be able to use those as sources and on a web server there's not likely
|
||||||
|
// going to be a ton of keyboard or mouse action. web servers do have one thing that we can use
|
||||||
|
// however, a ton of people visiting the website. obviously you don't want to base your seeding
|
||||||
|
// soley on parameters a potential attacker sends but (1) not everything in $_SERVER is controlled
|
||||||
|
// by the user and (2) this isn't just looking at the data sent by the current user - it's based
|
||||||
|
// on the data sent by all users. one user requests the page and a hash of their info is saved.
|
||||||
|
// another user visits the page and the serialization of their data is utilized along with the
|
||||||
|
// server envirnment stuff and a hash of the previous http request data (which itself utilizes
|
||||||
|
// a hash of the session data before that). certainly an attacker should be assumed to have
|
||||||
|
// full control over his own http requests. he, however, is not going to have control over
|
||||||
|
// everyone's http requests.
|
||||||
|
static $crypto = false, $v;
|
||||||
|
if ($crypto === false) {
|
||||||
|
// save old session data
|
||||||
|
$old_session_id = session_id();
|
||||||
|
$old_use_cookies = ini_get('session.use_cookies');
|
||||||
|
$old_session_cache_limiter = session_cache_limiter();
|
||||||
|
$_OLD_SESSION = isset($_SESSION) ? $_SESSION : false;
|
||||||
|
if ($old_session_id != '') {
|
||||||
|
session_write_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
session_id(1);
|
||||||
|
ini_set('session.use_cookies', 0);
|
||||||
|
session_cache_limiter('');
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$v = $seed = $_SESSION['seed'] = pack('H*', sha1(
|
||||||
|
(isset($_SERVER) ? phpseclib_safe_serialize($_SERVER) : '') .
|
||||||
|
(isset($_POST) ? phpseclib_safe_serialize($_POST) : '') .
|
||||||
|
(isset($_GET) ? phpseclib_safe_serialize($_GET) : '') .
|
||||||
|
(isset($_COOKIE) ? phpseclib_safe_serialize($_COOKIE) : '') .
|
||||||
|
phpseclib_safe_serialize($GLOBALS) .
|
||||||
|
phpseclib_safe_serialize($_SESSION) .
|
||||||
|
phpseclib_safe_serialize($_OLD_SESSION)
|
||||||
|
));
|
||||||
|
if (!isset($_SESSION['count'])) {
|
||||||
|
$_SESSION['count'] = 0;
|
||||||
|
}
|
||||||
|
$_SESSION['count']++;
|
||||||
|
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
// restore old session data
|
||||||
|
if ($old_session_id != '') {
|
||||||
|
session_id($old_session_id);
|
||||||
|
session_start();
|
||||||
|
ini_set('session.use_cookies', $old_use_cookies);
|
||||||
|
session_cache_limiter($old_session_cache_limiter);
|
||||||
|
} else {
|
||||||
|
if ($_OLD_SESSION !== false) {
|
||||||
|
$_SESSION = $_OLD_SESSION;
|
||||||
|
unset($_OLD_SESSION);
|
||||||
|
} else {
|
||||||
|
unset($_SESSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in SSH2 a shared secret and an exchange hash are generated through the key exchange process.
|
||||||
|
// the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C.
|
||||||
|
// if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the
|
||||||
|
// original hash and the current hash. we'll be emulating that. for more info see the following URL:
|
||||||
|
//
|
||||||
|
// http://tools.ietf.org/html/rfc4253#section-7.2
|
||||||
|
//
|
||||||
|
// see the is_string($crypto) part for an example of how to expand the keys
|
||||||
|
$key = pack('H*', sha1($seed . 'A'));
|
||||||
|
$iv = pack('H*', sha1($seed . 'C'));
|
||||||
|
|
||||||
|
// ciphers are used as per the nist.gov link below. also, see this link:
|
||||||
|
//
|
||||||
|
// http://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Designs_based_on_cryptographic_primitives
|
||||||
|
switch (true) {
|
||||||
|
case class_exists('\phpseclib\Crypt\AES'):
|
||||||
|
$crypto = new AES(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\Twofish'):
|
||||||
|
$crypto = new Twofish(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\Blowfish'):
|
||||||
|
$crypto = new Blowfish(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\TripleDES'):
|
||||||
|
$crypto = new TripleDES(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\DES'):
|
||||||
|
$crypto = new DES(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\RC4'):
|
||||||
|
$crypto = new RC4();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
user_error(__CLASS__ . ' requires at least one symmetric cipher be loaded');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$crypto->setKey($key);
|
||||||
|
$crypto->setIV($iv);
|
||||||
|
$crypto->enableContinuousBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
//return $crypto->encrypt(str_repeat("\0", $length));
|
||||||
|
|
||||||
|
// the following is based off of ANSI X9.31:
|
||||||
|
//
|
||||||
|
// http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf
|
||||||
|
//
|
||||||
|
// OpenSSL uses that same standard for it's random numbers:
|
||||||
|
//
|
||||||
|
// http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c
|
||||||
|
// (do a search for "ANS X9.31 A.2.4")
|
||||||
|
$result = '';
|
||||||
|
while (strlen($result) < $length) {
|
||||||
|
$i = $crypto->encrypt(microtime()); // strlen(microtime()) == 21
|
||||||
|
$r = $crypto->encrypt($i ^ $v); // strlen($v) == 20
|
||||||
|
$v = $crypto->encrypt($r ^ $i); // strlen($r) == 20
|
||||||
|
$result.= $r;
|
||||||
|
}
|
||||||
|
return substr($result, 0, $length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('phpseclib_safe_serialize')) {
|
||||||
|
/**
|
||||||
|
* Safely serialize variables
|
||||||
|
*
|
||||||
|
* If a class has a private __sleep() method it'll give a fatal error on PHP 5.2 and earlier.
|
||||||
|
* PHP 5.3 will emit a warning.
|
||||||
|
*
|
||||||
|
* @param mixed $arr
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function phpseclib_safe_serialize(&$arr)
|
||||||
|
{
|
||||||
|
if (is_object($arr)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (!is_array($arr)) {
|
||||||
|
return serialize($arr);
|
||||||
|
}
|
||||||
|
// prevent circular array recursion
|
||||||
|
if (isset($arr['__phpseclib_marker'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$safearr = array();
|
||||||
|
$arr['__phpseclib_marker'] = true;
|
||||||
|
foreach (array_keys($arr) as $key) {
|
||||||
|
// do not recurse on the '__phpseclib_marker' key itself, for smaller memory usage
|
||||||
|
if ($key !== '__phpseclib_marker') {
|
||||||
|
$safearr[$key] = phpseclib_safe_serialize($arr[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($arr['__phpseclib_marker']);
|
||||||
|
return serialize($safearr);
|
||||||
|
}
|
||||||
|
}
|
||||||
577
build/phpseclib/File/ANSI.php
Normal file
577
build/phpseclib/File/ANSI.php
Normal file
|
|
@ -0,0 +1,577 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP ANSI Decoder
|
||||||
|
*
|
||||||
|
* PHP version 5
|
||||||
|
*
|
||||||
|
* If you call read() in \phpseclib\Net\SSH2 you may get {@link http://en.wikipedia.org/wiki/ANSI_escape_code ANSI escape codes} back.
|
||||||
|
* They'd look like chr(0x1B) . '[00m' or whatever (0x1B = ESC). They tell a
|
||||||
|
* {@link http://en.wikipedia.org/wiki/Terminal_emulator terminal emulator} how to format the characters, what
|
||||||
|
* color to display them in, etc. \phpseclib\File\ANSI is a {@link http://en.wikipedia.org/wiki/VT100 VT100} terminal emulator.
|
||||||
|
*
|
||||||
|
* @category File
|
||||||
|
* @package ANSI
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @copyright 2012 Jim Wigginton
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @link http://phpseclib.sourceforge.net
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace phpseclib\File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP ANSI Decoder
|
||||||
|
*
|
||||||
|
* @package ANSI
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
class ANSI
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Max Width
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $max_x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max Height
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $max_y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max History
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $max_history;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $history;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History Attributes
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $history_attrs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current Column
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current Row
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old Column
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $old_x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old Row
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $old_y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An empty attribute cell
|
||||||
|
*
|
||||||
|
* @var object
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $base_attr_cell;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current attribute cell
|
||||||
|
*
|
||||||
|
* @var object
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $attr_cell;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An empty attribute row
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $attr_row;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current screen text
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $screen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current screen attributes
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $attrs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current ANSI code
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $ansi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenization
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $tokenization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Constructor.
|
||||||
|
*
|
||||||
|
* @return \phpseclib\File\ANSI
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function __construct()
|
||||||
|
{
|
||||||
|
$attr_cell = new \stdClass();
|
||||||
|
$attr_cell->bold = false;
|
||||||
|
$attr_cell->underline = false;
|
||||||
|
$attr_cell->blink = false;
|
||||||
|
$attr_cell->background = 'black';
|
||||||
|
$attr_cell->foreground = 'white';
|
||||||
|
$attr_cell->reverse = false;
|
||||||
|
$this->base_attr_cell = clone $attr_cell;
|
||||||
|
$this->attr_cell = clone $attr_cell;
|
||||||
|
|
||||||
|
$this->setHistory(200);
|
||||||
|
$this->setDimensions(80, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set terminal width and height
|
||||||
|
*
|
||||||
|
* Resets the screen as well
|
||||||
|
*
|
||||||
|
* @param int $x
|
||||||
|
* @param int $y
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function setDimensions($x, $y)
|
||||||
|
{
|
||||||
|
$this->max_x = $x - 1;
|
||||||
|
$this->max_y = $y - 1;
|
||||||
|
$this->x = $this->y = 0;
|
||||||
|
$this->history = $this->history_attrs = array();
|
||||||
|
$this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell);
|
||||||
|
$this->screen = array_fill(0, $this->max_y + 1, '');
|
||||||
|
$this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row);
|
||||||
|
$this->ansi = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the number of lines that should be logged past the terminal height
|
||||||
|
*
|
||||||
|
* @param int $history
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function setHistory($history)
|
||||||
|
{
|
||||||
|
$this->max_history = $history;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a string
|
||||||
|
*
|
||||||
|
* @param string $source
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function loadString($source)
|
||||||
|
{
|
||||||
|
$this->setDimensions($this->max_x + 1, $this->max_y + 1);
|
||||||
|
$this->appendString($source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appdend a string
|
||||||
|
*
|
||||||
|
* @param string $source
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function appendString($source)
|
||||||
|
{
|
||||||
|
$this->tokenization = array('');
|
||||||
|
for ($i = 0; $i < strlen($source); $i++) {
|
||||||
|
if (strlen($this->ansi)) {
|
||||||
|
$this->ansi.= $source[$i];
|
||||||
|
$chr = ord($source[$i]);
|
||||||
|
// http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements
|
||||||
|
// single character CSI's not currently supported
|
||||||
|
switch (true) {
|
||||||
|
case $this->ansi == "\x1B=":
|
||||||
|
$this->ansi = '';
|
||||||
|
continue 2;
|
||||||
|
case strlen($this->ansi) == 2 && $chr >= 64 && $chr <= 95 && $chr != ord('['):
|
||||||
|
case strlen($this->ansi) > 2 && $chr >= 64 && $chr <= 126:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
$this->tokenization[] = $this->ansi;
|
||||||
|
$this->tokenization[] = '';
|
||||||
|
// http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||||
|
switch ($this->ansi) {
|
||||||
|
case "\x1B[H": // Move cursor to upper left corner
|
||||||
|
$this->old_x = $this->x;
|
||||||
|
$this->old_y = $this->y;
|
||||||
|
$this->x = $this->y = 0;
|
||||||
|
break;
|
||||||
|
case "\x1B[J": // Clear screen from cursor down
|
||||||
|
$this->history = array_merge($this->history, array_slice(array_splice($this->screen, $this->y + 1), 0, $this->old_y));
|
||||||
|
$this->screen = array_merge($this->screen, array_fill($this->y, $this->max_y, ''));
|
||||||
|
|
||||||
|
$this->history_attrs = array_merge($this->history_attrs, array_slice(array_splice($this->attrs, $this->y + 1), 0, $this->old_y));
|
||||||
|
$this->attrs = array_merge($this->attrs, array_fill($this->y, $this->max_y, $this->attr_row));
|
||||||
|
|
||||||
|
if (count($this->history) == $this->max_history) {
|
||||||
|
array_shift($this->history);
|
||||||
|
array_shift($this->history_attrs);
|
||||||
|
}
|
||||||
|
case "\x1B[K": // Clear screen from cursor right
|
||||||
|
$this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x);
|
||||||
|
|
||||||
|
array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - ($this->x - 1), $this->base_attr_cell));
|
||||||
|
break;
|
||||||
|
case "\x1B[2K": // Clear entire line
|
||||||
|
$this->screen[$this->y] = str_repeat(' ', $this->x);
|
||||||
|
$this->attrs[$this->y] = $this->attr_row;
|
||||||
|
break;
|
||||||
|
case "\x1B[?1h": // set cursor key to application
|
||||||
|
case "\x1B[?25h": // show the cursor
|
||||||
|
case "\x1B(B": // set united states g0 character set
|
||||||
|
break;
|
||||||
|
case "\x1BE": // Move to next line
|
||||||
|
$this->_newLine();
|
||||||
|
$this->x = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
switch (true) {
|
||||||
|
case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines
|
||||||
|
$this->old_y = $this->y;
|
||||||
|
$this->y+= $match[1];
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h
|
||||||
|
$this->old_x = $this->x;
|
||||||
|
$this->old_y = $this->y;
|
||||||
|
$this->x = $match[2] - 1;
|
||||||
|
$this->y = $match[1] - 1;
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines
|
||||||
|
$this->old_x = $this->x;
|
||||||
|
$this->x+= $match[1];
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines
|
||||||
|
$this->old_x = $this->x;
|
||||||
|
$this->x-= $match[1];
|
||||||
|
if ($this->x < 0) {
|
||||||
|
$this->x = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes
|
||||||
|
$attr_cell = &$this->attr_cell;
|
||||||
|
$mods = explode(';', $match[1]);
|
||||||
|
foreach ($mods as $mod) {
|
||||||
|
switch ($mod) {
|
||||||
|
case '':
|
||||||
|
case '0': // Turn off character attributes
|
||||||
|
$attr_cell = clone $this->base_attr_cell;
|
||||||
|
break;
|
||||||
|
case '1': // Turn bold mode on
|
||||||
|
$attr_cell->bold = true;
|
||||||
|
break;
|
||||||
|
case '4': // Turn underline mode on
|
||||||
|
$attr_cell->underline = true;
|
||||||
|
break;
|
||||||
|
case '5': // Turn blinking mode on
|
||||||
|
$attr_cell->blink = true;
|
||||||
|
break;
|
||||||
|
case '7': // Turn reverse video on
|
||||||
|
$attr_cell->reverse = !$attr_cell->reverse;
|
||||||
|
$temp = $attr_cell->background;
|
||||||
|
$attr_cell->background = $attr_cell->foreground;
|
||||||
|
$attr_cell->foreground = $temp;
|
||||||
|
break;
|
||||||
|
default: // set colors
|
||||||
|
//$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground;
|
||||||
|
$front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' };
|
||||||
|
//$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background;
|
||||||
|
$back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' };
|
||||||
|
switch ($mod) {
|
||||||
|
// @codingStandardsIgnoreStart
|
||||||
|
case '30': $front = 'black'; break;
|
||||||
|
case '31': $front = 'red'; break;
|
||||||
|
case '32': $front = 'green'; break;
|
||||||
|
case '33': $front = 'yellow'; break;
|
||||||
|
case '34': $front = 'blue'; break;
|
||||||
|
case '35': $front = 'magenta'; break;
|
||||||
|
case '36': $front = 'cyan'; break;
|
||||||
|
case '37': $front = 'white'; break;
|
||||||
|
|
||||||
|
case '40': $back = 'black'; break;
|
||||||
|
case '41': $back = 'red'; break;
|
||||||
|
case '42': $back = 'green'; break;
|
||||||
|
case '43': $back = 'yellow'; break;
|
||||||
|
case '44': $back = 'blue'; break;
|
||||||
|
case '45': $back = 'magenta'; break;
|
||||||
|
case '46': $back = 'cyan'; break;
|
||||||
|
case '47': $back = 'white'; break;
|
||||||
|
// @codingStandardsIgnoreEnd
|
||||||
|
|
||||||
|
default:
|
||||||
|
//user_error('Unsupported attribute: ' . $mod);
|
||||||
|
$this->ansi = '';
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
//user_error("{$this->ansi} is unsupported\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->ansi = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tokenization[count($this->tokenization) - 1].= $source[$i];
|
||||||
|
switch ($source[$i]) {
|
||||||
|
case "\r":
|
||||||
|
$this->x = 0;
|
||||||
|
break;
|
||||||
|
case "\n":
|
||||||
|
$this->_newLine();
|
||||||
|
break;
|
||||||
|
case "\x08": // backspace
|
||||||
|
if ($this->x) {
|
||||||
|
$this->x--;
|
||||||
|
$this->attrs[$this->y][$this->x] = clone $this->base_attr_cell;
|
||||||
|
$this->screen[$this->y] = substr_replace(
|
||||||
|
$this->screen[$this->y],
|
||||||
|
$source[$i],
|
||||||
|
$this->x,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "\x0F": // shift
|
||||||
|
break;
|
||||||
|
case "\x1B": // start ANSI escape code
|
||||||
|
$this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1);
|
||||||
|
//if (!strlen($this->tokenization[count($this->tokenization) - 1])) {
|
||||||
|
// array_pop($this->tokenization);
|
||||||
|
//}
|
||||||
|
$this->ansi.= "\x1B";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->attrs[$this->y][$this->x] = clone $this->attr_cell;
|
||||||
|
if ($this->x > strlen($this->screen[$this->y])) {
|
||||||
|
$this->screen[$this->y] = str_repeat(' ', $this->x);
|
||||||
|
}
|
||||||
|
$this->screen[$this->y] = substr_replace(
|
||||||
|
$this->screen[$this->y],
|
||||||
|
$source[$i],
|
||||||
|
$this->x,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->x > $this->max_x) {
|
||||||
|
$this->x = 0;
|
||||||
|
$this->_newLine();
|
||||||
|
} else {
|
||||||
|
$this->x++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new line
|
||||||
|
*
|
||||||
|
* Also update the $this->screen and $this->history buffers
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
function _newLine()
|
||||||
|
{
|
||||||
|
//if ($this->y < $this->max_y) {
|
||||||
|
// $this->y++;
|
||||||
|
//}
|
||||||
|
|
||||||
|
while ($this->y >= $this->max_y) {
|
||||||
|
$this->history = array_merge($this->history, array(array_shift($this->screen)));
|
||||||
|
$this->screen[] = '';
|
||||||
|
|
||||||
|
$this->history_attrs = array_merge($this->history_attrs, array(array_shift($this->attrs)));
|
||||||
|
$this->attrs[] = $this->attr_row;
|
||||||
|
|
||||||
|
if (count($this->history) >= $this->max_history) {
|
||||||
|
array_shift($this->history);
|
||||||
|
array_shift($this->history_attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->y--;
|
||||||
|
}
|
||||||
|
$this->y++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current coordinate without preformating
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function _processCoordinate($last_attr, $cur_attr, $char)
|
||||||
|
{
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
if ($last_attr != $cur_attr) {
|
||||||
|
$close = $open = '';
|
||||||
|
if ($last_attr->foreground != $cur_attr->foreground) {
|
||||||
|
if ($cur_attr->foreground != 'white') {
|
||||||
|
$open.= '<span style="color: ' . $cur_attr->foreground . '">';
|
||||||
|
}
|
||||||
|
if ($last_attr->foreground != 'white') {
|
||||||
|
$close = '</span>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($last_attr->background != $cur_attr->background) {
|
||||||
|
if ($cur_attr->background != 'black') {
|
||||||
|
$open.= '<span style="background: ' . $cur_attr->background . '">';
|
||||||
|
}
|
||||||
|
if ($last_attr->background != 'black') {
|
||||||
|
$close = '</span>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($last_attr->bold != $cur_attr->bold) {
|
||||||
|
if ($cur_attr->bold) {
|
||||||
|
$open.= '<b>';
|
||||||
|
} else {
|
||||||
|
$close = '</b>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($last_attr->underline != $cur_attr->underline) {
|
||||||
|
if ($cur_attr->underline) {
|
||||||
|
$open.= '<u>';
|
||||||
|
} else {
|
||||||
|
$close = '</u>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($last_attr->blink != $cur_attr->blink) {
|
||||||
|
if ($cur_attr->blink) {
|
||||||
|
$open.= '<blink>';
|
||||||
|
} else {
|
||||||
|
$close = '</blink>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$output.= $close . $open;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output.= htmlspecialchars($char);
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current screen without preformating
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function _getScreen()
|
||||||
|
{
|
||||||
|
$output = '';
|
||||||
|
$last_attr = $this->base_attr_cell;
|
||||||
|
for ($i = 0; $i <= $this->max_y; $i++) {
|
||||||
|
for ($j = 0; $j <= $this->max_x; $j++) {
|
||||||
|
$cur_attr = $this->attrs[$i][$j];
|
||||||
|
$output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : '');
|
||||||
|
$last_attr = $this->attrs[$i][$j];
|
||||||
|
}
|
||||||
|
$output.= "\r\n";
|
||||||
|
}
|
||||||
|
$output = substr($output, 0, -2);
|
||||||
|
// close any remaining open tags
|
||||||
|
$output.= $this->_processCoordinate($last_attr, $this->base_attr_cell, '');
|
||||||
|
return rtrim($output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current screen
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getScreen()
|
||||||
|
{
|
||||||
|
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $this->_getScreen() . '</pre>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current screen and the x previous lines
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getHistory()
|
||||||
|
{
|
||||||
|
$scrollback = '';
|
||||||
|
$last_attr = $this->base_attr_cell;
|
||||||
|
for ($i = 0; $i < count($this->history); $i++) {
|
||||||
|
for ($j = 0; $j <= $this->max_x + 1; $j++) {
|
||||||
|
$cur_attr = $this->history_attrs[$i][$j];
|
||||||
|
$scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : '');
|
||||||
|
$last_attr = $this->history_attrs[$i][$j];
|
||||||
|
}
|
||||||
|
$scrollback.= "\r\n";
|
||||||
|
}
|
||||||
|
$base_attr_cell = $this->base_attr_cell;
|
||||||
|
$this->base_attr_cell = $last_attr;
|
||||||
|
$scrollback.= $this->_getScreen();
|
||||||
|
$this->base_attr_cell = $base_attr_cell;
|
||||||
|
|
||||||
|
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $scrollback . '</span></pre>';
|
||||||
|
}
|
||||||
|
}
|
||||||
1450
build/phpseclib/File/ASN1.php
Normal file
1450
build/phpseclib/File/ASN1.php
Normal file
File diff suppressed because it is too large
Load diff
47
build/phpseclib/File/ASN1/Element.php
Normal file
47
build/phpseclib/File/ASN1/Element.php
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pure-PHP ASN.1 Parser
|
||||||
|
*
|
||||||
|
* PHP version 5
|
||||||
|
*
|
||||||
|
* @category File
|
||||||
|
* @package ASN1
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @copyright 2012 Jim Wigginton
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @link http://phpseclib.sourceforge.net
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace phpseclib\File\ASN1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASN.1 Element
|
||||||
|
*
|
||||||
|
* Bypass normal encoding rules in phpseclib\File\ASN1::encodeDER()
|
||||||
|
*
|
||||||
|
* @package ASN1
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
class Element
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Raw element value
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param string $encoded
|
||||||
|
* @return \phpseclib\File\ASN1\Element
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function __construct($encoded)
|
||||||
|
{
|
||||||
|
$this->element = $encoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
5097
build/phpseclib/File/X509.php
Normal file
5097
build/phpseclib/File/X509.php
Normal file
File diff suppressed because it is too large
Load diff
3787
build/phpseclib/Math/BigInteger.php
Normal file
3787
build/phpseclib/Math/BigInteger.php
Normal file
File diff suppressed because it is too large
Load diff
6
build/phpseclib/openssl.cnf
Normal file
6
build/phpseclib/openssl.cnf
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# minimalist openssl.cnf file for use with phpseclib
|
||||||
|
|
||||||
|
HOME = .
|
||||||
|
RANDFILE = $ENV::HOME/.rnd
|
||||||
|
|
||||||
|
[ v3_ca ]
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
define('ROOT_DIR', dirname(__DIR__));
|
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
|
||||||
define('PLUGINS_DEST_DIR', dirname(__DIR__) . '/build/dist/releases/plugins');
|
|
||||||
|
|
||||||
$destPath = __DIR__ . 'build/dist/releases/plugins/';
|
|
||||||
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
|
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
|
||||||
|
$files = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator(PLUGINS_DEST_DIR, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
foreach ($files as $fileinfo) {
|
||||||
|
$fileinfo->isDir() || unlink($fileinfo->getRealPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
$terser = ROOT_DIR . '/node_modules/terser/bin/terser';
|
||||||
|
|
||||||
$manifest = [];
|
$manifest = [];
|
||||||
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
|
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
|
||||||
$keys = [
|
$keys = [
|
||||||
|
|
@ -20,10 +28,46 @@ $keys = [
|
||||||
'url',
|
'url',
|
||||||
'version'
|
'version'
|
||||||
];
|
];
|
||||||
|
/*
|
||||||
|
$released = [
|
||||||
|
'add-x-originating-ip-header',
|
||||||
|
'avatars',
|
||||||
|
'backup',
|
||||||
|
'black-list',
|
||||||
|
'change-password',
|
||||||
|
'change-password-froxlor',
|
||||||
|
'change-password-hestia',
|
||||||
|
'change-password-hmailserver',
|
||||||
|
'change-password-ispconfig',
|
||||||
|
'change-password-poppassd',
|
||||||
|
'custom-login-mapping',
|
||||||
|
'imap-contacts-suggestions',
|
||||||
|
'kolab',
|
||||||
|
'ldap-contacts-suggestions',
|
||||||
|
'ldap-identities',
|
||||||
|
'ldap-login-mapping',
|
||||||
|
'ldap-mail-accounts',
|
||||||
|
'login-external',
|
||||||
|
'login-external-sso',
|
||||||
|
'login-override',
|
||||||
|
'login-register',
|
||||||
|
'login-remote',
|
||||||
|
'mailbox-detect',
|
||||||
|
'nextcloud',
|
||||||
|
'override-smtp-credentials',
|
||||||
|
'set-remote-addr',
|
||||||
|
'smtp-use-from-adr-account',
|
||||||
|
'snowfall-on-login-screen',
|
||||||
|
'two-factor-auth',
|
||||||
|
'view-ics',
|
||||||
|
'white-list'
|
||||||
|
];
|
||||||
|
*/
|
||||||
foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
||||||
if (is_file("{$dir}/index.php")) {
|
if (is_file("{$dir}/index.php") && !strpos($dir, '.bak')) {
|
||||||
require "{$dir}/index.php";
|
require "{$dir}/index.php";
|
||||||
$name = basename($dir);
|
$name = basename($dir);
|
||||||
|
// if (!in_array($name, $released)) continue;
|
||||||
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
|
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
|
||||||
$manifest_item = [];
|
$manifest_item = [];
|
||||||
foreach ($class->getConstants() as $key => $value) {
|
foreach ($class->getConstants() as $key => $value) {
|
||||||
|
|
@ -35,29 +79,51 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
||||||
$version = $manifest_item['version'];
|
$version = $manifest_item['version'];
|
||||||
if (0 < floatval($version)) {
|
if (0 < floatval($version)) {
|
||||||
echo "+ {$name} {$version}\n";
|
echo "+ {$name} {$version}\n";
|
||||||
|
|
||||||
|
// Minify JavaScript
|
||||||
|
foreach (glob("{$dir}/*.js") as $file) {
|
||||||
|
if (!strpos($file,'.min')) {
|
||||||
|
$mfile = str_replace('.js', '.min.js', $file);
|
||||||
|
passthru("{$terser} {$file} --output {$mfile} --compress 'drop_console' --ecma 6 --mangle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (glob("{$dir}/js/*.js") as $file) {
|
||||||
|
if (!strpos($file,'.min')) {
|
||||||
|
$mfile = str_replace('.js', '.min.js', $file);
|
||||||
|
passthru("{$terser} {$file} --output {$mfile} --compress 'drop_console' --ecma 6 --mangle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$manifest_item['type'] = 'plugin';
|
$manifest_item['type'] = 'plugin';
|
||||||
$manifest_item['id'] = $name;
|
$manifest_item['id'] = $name;
|
||||||
$manifest_item['file'] = "{$dir}-{$version}.tgz";
|
$manifest_item['file'] = "plugins/{$name}-{$version}.tgz";
|
||||||
ksort($manifest_item);
|
|
||||||
$manifest[$name] = $manifest_item;
|
|
||||||
$tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar";
|
$tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar";
|
||||||
$tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz";
|
$tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz";
|
||||||
@unlink($tgz_destination);
|
@unlink($tgz_destination);
|
||||||
@unlink("{$tar_destination}.gz");
|
@unlink("{$tar_destination}.gz");
|
||||||
$tar = new PharData($tar_destination);
|
$tar = new PharData($tar_destination);
|
||||||
$tar->buildFromDirectory('./plugins/', '/' . \preg_quote("./plugins/{$name}", '/') . '/');
|
$tar->buildFromDirectory('./plugins/', '/' . \preg_quote("./plugins/{$name}/", '/') . '((?!\.bak).)*$/');
|
||||||
$tar->compress(Phar::GZ);
|
$tar->compress(Phar::GZ);
|
||||||
unlink($tar_destination);
|
unlink($tar_destination);
|
||||||
rename("{$tar_destination}.gz", $tgz_destination);
|
rename("{$tar_destination}.gz", $tgz_destination);
|
||||||
|
/*
|
||||||
if (Phar::canWrite()) {
|
if (Phar::canWrite()) {
|
||||||
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
|
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
|
||||||
@unlink($phar_destination);
|
@unlink($phar_destination);
|
||||||
$tar = new Phar($phar_destination);
|
$tar = new Phar($phar_destination);
|
||||||
$tar->buildFromDirectory("./plugins/{$name}/");
|
$tar->buildFromDirectory("./plugins/{$name}/", '/^((?!\.bak).)*$/');
|
||||||
$tar->compress(Phar::GZ);
|
$tar->compress(Phar::GZ);
|
||||||
unlink($phar_destination);
|
unlink($phar_destination);
|
||||||
rename("{$phar_destination}.gz", $phar_destination);
|
rename("{$phar_destination}.gz", $phar_destination);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
if (isset($options['sign'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tgz_destination), $return_var);
|
||||||
|
$manifest_item['pgp_sig'] = trim(preg_replace('/-----(BEGIN|END) PGP SIGNATURE-----/', '', file_get_contents($tgz_destination.'.asc')));
|
||||||
|
}
|
||||||
|
ksort($manifest_item);
|
||||||
|
$manifest[$name] = $manifest_item;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
echo "- {$name} {$version}\n";
|
echo "- {$name} {$version}\n";
|
||||||
}
|
}
|
||||||
|
|
@ -73,5 +139,6 @@ $manifest = str_replace('"}', "\"\n\t}", $manifest);
|
||||||
$manifest = str_replace('}]', "}\n]", $manifest);
|
$manifest = str_replace('}]', "}\n]", $manifest);
|
||||||
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
|
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
|
||||||
$manifest = str_replace('\/', '/', $manifest);
|
$manifest = str_replace('\/', '/', $manifest);
|
||||||
file_put_contents(PLUGINS_DEST_DIR . "/packages.json", $manifest);
|
file_put_contents(dirname(PLUGINS_DEST_DIR) . "/packages.json", $manifest);
|
||||||
|
|
||||||
exit;
|
exit;
|
||||||
|
|
|
||||||
247
cli/compile.php
Executable file
247
cli/compile.php
Executable file
|
|
@ -0,0 +1,247 @@
|
||||||
|
#!/usr/bin/php
|
||||||
|
<?php
|
||||||
|
define('ROOT_DIR', dirname(__DIR__));
|
||||||
|
chdir(ROOT_DIR);
|
||||||
|
|
||||||
|
$options = getopt('', ['readme','source','knockout','openpgp','squire']);
|
||||||
|
$return_var = '';
|
||||||
|
|
||||||
|
function execCmd($cmd)
|
||||||
|
{
|
||||||
|
passthru($cmd, $return_var);
|
||||||
|
if ($return_var) {
|
||||||
|
exit("`{$cmd}` failed with error code {$return_var}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['knockout'])) {
|
||||||
|
chdir(ROOT_DIR . '/vendors/knockout');
|
||||||
|
if ($grunt = trim(`which grunt`)) {
|
||||||
|
execCmd($grunt, $return_var);
|
||||||
|
} else {
|
||||||
|
exit('grunt not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = ROOT_DIR . '/vendors/knockout/build/output/knockout-latest.js';
|
||||||
|
$data = file_get_contents($file);
|
||||||
|
/*
|
||||||
|
Strip start: (()=>{'use strict';var aa="function"==typeof Object.defineProperties?Object.defineProperty:function(C,F,E){if(C==Array.prototype||C==Object.prototype)return C;C[F]=E.value;return C};function ba(C){C=["object"==typeof globalThis&&globalThis,C,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var F=0;F<C.length;++F){var E=C[F];if(E&&E.Math==Math)return E}throw Error("Cannot find global object");}var ca=ba(this);
|
||||||
|
function Z(C,F){if(F)a:{var E=ca;C=C.split(".");for(var J=0;J<C.length-1;J++){var S=C[J];if(!(S in E))break a;E=E[S]}C=C[C.length-1];J=E[C];F=F(J);F!=J&&null!=F&&aa(E,C,{configurable:!0,writable:!0,value:F})}}Z("Object.entries",function(C){return C?C:function(F){var E=[],J;for(J in F)Object.prototype.hasOwnProperty.call(F,J)&&E.push([J,F[J]]);return E}});
|
||||||
|
Z("Array.prototype.includes",function(C){return C?C:function(F,E){var J=this;J instanceof String&&(J=String(J));var S=J.length;E=E||0;for(0>E&&(E=Math.max(E+S,0));E<S;E++){var V=J[E];if(V===F||Object.is(V,F))return!0}return!1}});
|
||||||
|
Strip end: })();
|
||||||
|
*/
|
||||||
|
|
||||||
|
$p1 = strpos($data, '(()=>');
|
||||||
|
// ECMASCRIPT_202
|
||||||
|
$data = substr($data, 0, $p1) . substr($data, $p1 + 6, -6) . "\n";
|
||||||
|
// ECMASCRIPT_2018
|
||||||
|
// $data = substr($data, 0, $p1) . substr($data, $p1 + 19, -6) . "\n";
|
||||||
|
// ECMASCRIPT6
|
||||||
|
// $data = substr($data, 0, $p1) . substr($data, strpos($data, 'return!0}return!1}});') + 22, -6) . "\n";
|
||||||
|
|
||||||
|
file_put_contents($file, $data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['openpgp'])) {
|
||||||
|
chdir(ROOT_DIR . '/vendors/openpgp-5');
|
||||||
|
execCmd('git pull', $return_var);
|
||||||
|
execCmd('./build.sh', $return_var);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['squire'])) {
|
||||||
|
chdir(ROOT_DIR . '/vendors/squire2');
|
||||||
|
// execCmd('git pull', $return_var);
|
||||||
|
// execCmd('git checkout snappymail', $return_var);
|
||||||
|
execCmd('./build.sh', $return_var);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['source'])) {
|
||||||
|
#cd ROOT_DIR . '/vendors/openpgp-2.6.2/';
|
||||||
|
#rollup -c
|
||||||
|
#npm cache clean --force
|
||||||
|
|
||||||
|
if ($gulp = trim(`which gulp`)) {
|
||||||
|
execCmd($gulp, $return_var);
|
||||||
|
} else {
|
||||||
|
exit('gulp not installed, run as root: npm install --global gulp-cli');
|
||||||
|
}
|
||||||
|
|
||||||
|
#rollup -c
|
||||||
|
|
||||||
|
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
|
||||||
|
|
||||||
|
if ($gzip = trim(`which gzip`)) {
|
||||||
|
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
|
||||||
|
// execCmd("{$gzip} -k --best {$cmddir}/js/*.js");
|
||||||
|
execCmd("{$gzip} -k --best {$cmddir}/js/min/*.js");
|
||||||
|
execCmd("{$gzip} -k --best {$cmddir}/css/admin*.css");
|
||||||
|
execCmd("{$gzip} -k --best {$cmddir}/css/app*.css");
|
||||||
|
// unlink($cmddir . '/js/boot.js.gz');
|
||||||
|
// unlink($cmddir . '/js/min/boot.min.js.gz');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($brotli = trim(`which brotli`)) {
|
||||||
|
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
|
||||||
|
// execCmd("{$brotli} -k --best {$cmddir}/js/*.js");
|
||||||
|
execCmd("{$brotli} -k --best {$cmddir}/js/min/*.js");
|
||||||
|
execCmd("{$brotli} -k --best {$cmddir}/css/admin*.css");
|
||||||
|
execCmd("{$brotli} -k --best {$cmddir}/css/app*.css");
|
||||||
|
// unlink($cmddir . '/js/boot.js.br');
|
||||||
|
// unlink($cmddir . '/js/min/boot.min.js.br');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['readme'])) {
|
||||||
|
|
||||||
|
$basedir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0';
|
||||||
|
|
||||||
|
$readme = file_get_contents(ROOT_DIR . '/README.md');
|
||||||
|
|
||||||
|
$total = filesize("{$basedir}/static/js/serviceworker.js");
|
||||||
|
foreach (['admin','app','boot','libs','sieve'] as $name) {
|
||||||
|
$size = filesize("{$basedir}/static/js/{$name}.js");
|
||||||
|
$total += $size;
|
||||||
|
$readme = preg_replace("/(\\|{$name}\\.js\\s+\\|[\\s0-9.]+\\|)[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1' . str_pad(number_format($size, 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' |',
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$readme = preg_replace("/(\\|TOTAL\\s+\\|8[\\s0-9.]+\\|)[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1' . str_pad(number_format($total, 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' |',
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
|
||||||
|
$total = [];
|
||||||
|
$total_br = [];
|
||||||
|
$total_gz = [];
|
||||||
|
foreach (['admin','app','boot','libs','sieve'] as $name) {
|
||||||
|
$size = filesize("{$basedir}/static/js/min/{$name}.min.js");
|
||||||
|
$size_br = filesize("{$basedir}/static/js/min/{$name}.min.js.br");
|
||||||
|
$size_gz = filesize("{$basedir}/static/js/min/{$name}.min.js.gz");
|
||||||
|
$total[$name] = $size;
|
||||||
|
$total_br[$name] = $size_br;
|
||||||
|
$total_gz[$name] = $size_gz;
|
||||||
|
$readme = preg_replace(
|
||||||
|
"/(\\|{$name}\\.min\\.js\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1|'
|
||||||
|
. str_pad(number_format($size, 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$2|'
|
||||||
|
. str_pad(number_format($size_gz, 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$3|'
|
||||||
|
. str_pad(number_format($size_br, 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '|'
|
||||||
|
,
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$readme = preg_replace(
|
||||||
|
"/(TOTAL user\+sieve\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1|'
|
||||||
|
. str_pad(number_format(array_sum($total) - $total['admin'], 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$2|'
|
||||||
|
. str_pad(number_format(array_sum($total_gz) - $total_gz['admin'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$3|'
|
||||||
|
. str_pad(number_format(array_sum($total_br) - $total_br['admin'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '|'
|
||||||
|
,
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
|
||||||
|
//$smaller = 1217635 / (array_sum($total) - $total['admin']);
|
||||||
|
$smaller = 358761 / (array_sum($total_gz) - $total_gz['admin']);
|
||||||
|
//$smaller = 299485 / (array_sum($total_br) - $total_br['admin']);
|
||||||
|
$readme = preg_replace(
|
||||||
|
'/(For a user it is around) [0-9]+/',
|
||||||
|
'$1 ' . round(100 - 100 / $smaller),
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
|
||||||
|
unset($total['sieve']);
|
||||||
|
unset($total_br['sieve']);
|
||||||
|
unset($total_gz['sieve']);
|
||||||
|
$readme = preg_replace(
|
||||||
|
"/(TOTAL user\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1|'
|
||||||
|
. str_pad(number_format(array_sum($total) - $total['admin'], 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$2|'
|
||||||
|
. str_pad(number_format(array_sum($total_gz) - $total_gz['admin'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$3|'
|
||||||
|
. str_pad(number_format(array_sum($total_br) - $total_br['admin'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '|'
|
||||||
|
,
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
$readme = preg_replace(
|
||||||
|
"/(TOTAL admin\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1|'
|
||||||
|
. str_pad(number_format(array_sum($total) - $total['app'], 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$2|'
|
||||||
|
. str_pad(number_format(array_sum($total_gz) - $total_gz['app'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$3|'
|
||||||
|
. str_pad(number_format(array_sum($total_br) - $total_br['app'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '|'
|
||||||
|
,
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (['admin','app'] as $name) {
|
||||||
|
$readme = preg_replace(
|
||||||
|
"/(\\|{$name}\\.css\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1|'
|
||||||
|
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.css"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$2|'
|
||||||
|
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.css.gz"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' |'
|
||||||
|
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.css.br"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '|'
|
||||||
|
,
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
$readme = preg_replace(
|
||||||
|
"/(\\|{$name}\\.min\\.css\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1|'
|
||||||
|
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.min.css"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$2|'
|
||||||
|
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.min.css.gz"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' |'
|
||||||
|
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.min.css.br"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '|'
|
||||||
|
,
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$sizes = [
|
||||||
|
filesize("{$basedir}/static/js/min/openpgp.min.js"),
|
||||||
|
filesize("{$basedir}/static/js/min/openpgp.min.js.gz"),
|
||||||
|
filesize("{$basedir}/static/js/min/openpgp.min.js.br")
|
||||||
|
];
|
||||||
|
//|openpgp.min.js | 330.742 | 540.542 |102.388 | 167.397 | 84.241 | 137.502 |
|
||||||
|
$readme = preg_replace(
|
||||||
|
"/(openpgp\.min\.js\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
|
||||||
|
'$1|'
|
||||||
|
. str_pad(number_format($sizes[0], 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$2| '
|
||||||
|
. str_pad(number_format($sizes[1], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '$3| '
|
||||||
|
. str_pad(number_format($sizes[2], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
|
||||||
|
. '|'
|
||||||
|
,
|
||||||
|
$readme
|
||||||
|
);
|
||||||
|
|
||||||
|
//file://
|
||||||
|
|
||||||
|
file_put_contents(ROOT_DIR . '/README.md', $readme);
|
||||||
|
|
||||||
|
$data = file_put_contents(
|
||||||
|
"{$basedir}/app/templates/Views/Admin/AdminSettingsAbout.html",
|
||||||
|
preg_replace(
|
||||||
|
'/20[0-9][0-9] ©/',
|
||||||
|
date('Y').' ©',
|
||||||
|
file_get_contents("{$basedir}/app/templates/Views/Admin/AdminSettingsAbout.html")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
280
cli/release.php
Executable file
280
cli/release.php
Executable file
|
|
@ -0,0 +1,280 @@
|
||||||
|
#!/usr/bin/php
|
||||||
|
<?php
|
||||||
|
define('ROOT_DIR', dirname(__DIR__));
|
||||||
|
chdir(ROOT_DIR);
|
||||||
|
|
||||||
|
$options = getopt('', ['aur','docker','plugins','skip-gulp','debian','nextcloud','owncloud','cpanel','sign']);
|
||||||
|
|
||||||
|
if (isset($options['plugins'])) {
|
||||||
|
require(ROOT_DIR . '/build/plugins.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$gulp = trim(shell_exec('which gulp'));
|
||||||
|
if (!$gulp) {
|
||||||
|
exit('gulp not installed, run as root: npm install --global gulp-cli');
|
||||||
|
}
|
||||||
|
|
||||||
|
$package = json_decode(file_get_contents('package.json'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update files that contain version
|
||||||
|
*/
|
||||||
|
// cloudron
|
||||||
|
$file = ROOT_DIR . '/integrations/cloudron/Dockerfile';
|
||||||
|
file_put_contents($file, preg_replace('/VERSION=[0-9.]+/', "VERSION={$package->version}", file_get_contents($file)));
|
||||||
|
$file = ROOT_DIR . '/integrations/cloudron/DESCRIPTION.md';
|
||||||
|
file_put_contents($file, preg_replace('/<upstream>[^<]*</', "<upstream>{$package->version}<", file_get_contents($file)));
|
||||||
|
// virtualmin
|
||||||
|
$file = ROOT_DIR . '/integrations/virtualmin/snappymail.pl';
|
||||||
|
file_put_contents($file, preg_replace('/return \\( "[0-9]+\\.[0-9]+\\.[0-9]+" \\)/', "return ( \"{$package->version}\" )", file_get_contents($file)));
|
||||||
|
|
||||||
|
// Arch User Repository
|
||||||
|
// https://aur.archlinux.org/packages/snappymail/
|
||||||
|
$options['aur'] = isset($options['aur']);
|
||||||
|
|
||||||
|
// Docker build
|
||||||
|
$options['docker'] = isset($options['docker']);
|
||||||
|
if ($options['docker'] && !($docker = trim(shell_exec('which docker')))) {
|
||||||
|
exit('docker not found');
|
||||||
|
}
|
||||||
|
if ($options['docker'] && $options['aur']) {
|
||||||
|
exit('Conflict between docker and aur');
|
||||||
|
}
|
||||||
|
|
||||||
|
$destPath = "build/dist/releases/webmail/{$package->version}/";
|
||||||
|
is_dir($destPath) || mkdir($destPath, 0777, true);
|
||||||
|
|
||||||
|
$zip_destination = "{$destPath}snappymail-{$package->version}.zip";
|
||||||
|
$tar_destination = "{$destPath}snappymail-{$package->version}.tar";
|
||||||
|
|
||||||
|
@unlink($zip_destination);
|
||||||
|
@unlink($tar_destination);
|
||||||
|
@unlink("{$tar_destination}.gz");
|
||||||
|
|
||||||
|
if (!isset($options['skip-gulp'])) {
|
||||||
|
echo "\x1b[33;1m === Gulp === \x1b[0m\n";
|
||||||
|
passthru($gulp, $return_var);
|
||||||
|
if ($return_var) {
|
||||||
|
exit("gulp failed with error code {$return_var}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
|
||||||
|
|
||||||
|
if ($gzip = trim(shell_exec('which gzip'))) {
|
||||||
|
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
|
||||||
|
passthru("{$gzip} -k --best {$cmddir}/js/*.js");
|
||||||
|
passthru("{$gzip} -k --best {$cmddir}/js/min/*.js");
|
||||||
|
passthru("{$gzip} -k --best {$cmddir}/css/admin*.css");
|
||||||
|
passthru("{$gzip} -k --best {$cmddir}/css/app*.css");
|
||||||
|
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.gz');
|
||||||
|
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($brotli = trim(shell_exec('which brotli'))) {
|
||||||
|
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
|
||||||
|
passthru("{$brotli} -k --best {$cmddir}/js/*.js");
|
||||||
|
passthru("{$brotli} -k --best {$cmddir}/js/min/*.js");
|
||||||
|
passthru("{$brotli} -k --best {$cmddir}/css/admin*.css");
|
||||||
|
passthru("{$brotli} -k --best {$cmddir}/css/app*.css");
|
||||||
|
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.br');
|
||||||
|
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.br');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary rename folder to speed up PharData
|
||||||
|
//if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")){
|
||||||
|
if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")) {
|
||||||
|
exit('Failed to temporary rename snappymail/v/0.0.0');
|
||||||
|
}
|
||||||
|
register_shutdown_function(function(){
|
||||||
|
// Rename folder back to original
|
||||||
|
@rename("snappymail/v/{$GLOBALS['package']->version}", 'snappymail/v/0.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
echo "\x1b[33;1m === Zip/Tar === \x1b[0m\n";
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if (!$zip->open($zip_destination, ZIPARCHIVE::CREATE)) {
|
||||||
|
exit("Failed to create {$zip_destination}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$tar = new PharData($tar_destination);
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$file = str_replace('\\', '/', $file);
|
||||||
|
//echo "{$file}\n";
|
||||||
|
// Ignore "." and ".." folders
|
||||||
|
if (!in_array(substr($file, strrpos($file, '/')+1), array('.', '..'))) {
|
||||||
|
if (is_dir($file)) {
|
||||||
|
$zip->addEmptyDir($file);
|
||||||
|
} else if (is_file($file)) {
|
||||||
|
$zip->addFile($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['docker']) {
|
||||||
|
$tar->buildFromDirectory('./snappymail/', "@v/{$package->version}@");
|
||||||
|
} else {
|
||||||
|
$tar->buildFromDirectory('./', "@snappymail/v/{$package->version}@");
|
||||||
|
}
|
||||||
|
|
||||||
|
//$zip->addFile('cli/upgrade.sh');
|
||||||
|
//$tar->addFile('cli/upgrade.sh');
|
||||||
|
|
||||||
|
$zip->addFile('data/.htaccess');
|
||||||
|
$tar->addFile('data/.htaccess');
|
||||||
|
|
||||||
|
$zip->addFromString('data/VERSION', $package->version);
|
||||||
|
$tar->addFromString('data/VERSION', $package->version);
|
||||||
|
|
||||||
|
$zip->addFile('data/README.md');
|
||||||
|
$tar->addFile('data/README.md');
|
||||||
|
|
||||||
|
if ($options['aur']) {
|
||||||
|
$data = '<?php
|
||||||
|
function __get_custom_data_full_path()
|
||||||
|
{
|
||||||
|
return \'/var/lib/snappymail\';
|
||||||
|
}
|
||||||
|
';
|
||||||
|
$zip->addFromString('include.php', $data);
|
||||||
|
$tar->addFromString('include.php', $data);
|
||||||
|
} else {
|
||||||
|
$zip->addFile('_include.php');
|
||||||
|
$tar->addFile('_include.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->addFile('.htaccess');
|
||||||
|
$tar->addFile('.htaccess');
|
||||||
|
|
||||||
|
$index = file_get_contents('index.php');
|
||||||
|
$index = str_replace('0.0.0', $package->version, $index);
|
||||||
|
$zip->addFromString('index.php', $index);
|
||||||
|
$tar->addFromString('index.php', $index);
|
||||||
|
|
||||||
|
$zip->addFile('README.md');
|
||||||
|
$tar->addFile('README.md');
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$tar->compress(Phar::GZ);
|
||||||
|
unlink($tar_destination);
|
||||||
|
$tar_destination .= '.gz';
|
||||||
|
|
||||||
|
echo "{$zip_destination} created\n{$tar_destination} created\n";
|
||||||
|
|
||||||
|
if (isset($options['nextcloud'])) {
|
||||||
|
require(ROOT_DIR . '/build/nextcloud.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['owncloud'])) {
|
||||||
|
require(ROOT_DIR . '/build/owncloud.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['cpanel'])) {
|
||||||
|
require(ROOT_DIR . '/build/cpanel.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
rename("snappymail/v/{$package->version}", 'snappymail/v/0.0.0');
|
||||||
|
|
||||||
|
file_put_contents("{$destPath}core.json", '{
|
||||||
|
"version": "'.$package->version.'",
|
||||||
|
"file": "../latest.tar.gz",
|
||||||
|
"warnings": []
|
||||||
|
}');
|
||||||
|
|
||||||
|
// Arch User Repository
|
||||||
|
if ($options['aur']) {
|
||||||
|
// extension_loaded('blake2')
|
||||||
|
if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) {
|
||||||
|
function b2sum($file) {
|
||||||
|
$file = escapeshellarg($file);
|
||||||
|
exec("b2sum --binary {$file} 2>&1", $output, $exitcode);
|
||||||
|
$output = explode(' ', implode("\n", $output));
|
||||||
|
return $output[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$b2sums = function_exists('b2sum') ? [
|
||||||
|
b2sum($tar_destination),
|
||||||
|
b2sum(ROOT_DIR . '/build/arch/snappymail.sysusers'),
|
||||||
|
b2sum(ROOT_DIR . '/build/arch/snappymail.tmpfiles')
|
||||||
|
] : [];
|
||||||
|
|
||||||
|
file_put_contents('build/arch/.SRCINFO', 'pkgbase = snappymail
|
||||||
|
pkgdesc = modern PHP webmail client
|
||||||
|
pkgver = '.$package->version.'
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://github.com/the-djmaze/snappymail
|
||||||
|
arch = any
|
||||||
|
license = AGPL3
|
||||||
|
makedepends = php
|
||||||
|
makedepends = nodejs
|
||||||
|
makedepends = yarn
|
||||||
|
makedepends = gulp
|
||||||
|
depends = php-fpm
|
||||||
|
optdepends = mariadb: storage backend for contacts
|
||||||
|
optdepends = php-pgsql: storage backend for contacts
|
||||||
|
optdepends = php-sqlite: storage backend for contacts
|
||||||
|
source = snappymail-'.$package->version.'.tar.gz::https://github.com/the-djmaze/snappymail/archive/v'.$package->version.'.tar.gz
|
||||||
|
source = snappymail.sysusers
|
||||||
|
source = snappymail.tmpfiles
|
||||||
|
b2sums = '.implode("\n b2sums = ", $b2sums).'
|
||||||
|
|
||||||
|
pkgname = snappymail
|
||||||
|
');
|
||||||
|
|
||||||
|
$file = ROOT_DIR . '/build/arch/PKGBUILD';
|
||||||
|
if (is_file($file)) {
|
||||||
|
$PKGBUILD = file_get_contents($file);
|
||||||
|
$PKGBUILD = preg_replace('/pkgver=[0-9.]+/', "pkgver={$package->version}", $PKGBUILD);
|
||||||
|
$PKGBUILD = preg_replace('/b2sums=\\([^)]+\\)/s', "b2sums=('".implode("'\n '", $b2sums)."')", $PKGBUILD);
|
||||||
|
file_put_contents($file, $PKGBUILD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Debian Repository
|
||||||
|
else if (isset($options['debian'])) {
|
||||||
|
require(ROOT_DIR . '/build/deb.php');
|
||||||
|
}
|
||||||
|
// Docker build
|
||||||
|
else if ($options['docker']) {
|
||||||
|
echo "\x1b[33;1m === Docker === \x1b[0m\n";
|
||||||
|
$zip_filename = "snappymail-{$package->version}.zip";
|
||||||
|
copy($zip_destination, "./.docker/release/{$zip_filename}");
|
||||||
|
if ($docker) {
|
||||||
|
passthru("{$docker} build --pull " . ROOT_DIR . "/.docker/release/ --build-arg FILES_ZIP={$zip_filename} -t snappymail:{$package->version}");
|
||||||
|
} else {
|
||||||
|
echo "Docker not installed!\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['sign'])) {
|
||||||
|
echo "\x1b[33;1m === PGP Sign === \x1b[0m\n";
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tar_destination), $return_var);
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($zip_destination), $return_var);
|
||||||
|
if (isset($options['nextcloud'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
||||||
|
.escapeshellarg("{$destPath}snappymail-{$package->version}-nextcloud.tar.gz"), $return_var);
|
||||||
|
}
|
||||||
|
if (isset($options['owncloud'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
||||||
|
.escapeshellarg("{$destPath}snappymail-{$package->version}-owncloud.tar.gz"), $return_var);
|
||||||
|
}
|
||||||
|
if (isset($options['cpanel'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
||||||
|
.escapeshellarg("{$destPath}snappymail-{$package->version}-cpanel.tar.gz"), $return_var);
|
||||||
|
}
|
||||||
|
if (isset($options['debian'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/" . basename(DEB_DEST_DIR.'.deb')), $return_var);
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 --clearsign --output '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/InRelease") . ' '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 -abs --output '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release.gpg") . ' '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
cli/upgrade.sh
Executable file
51
cli/upgrade.sh
Executable file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# UPGRADING Snappymail
|
||||||
|
#
|
||||||
|
# I've found this to be working fine when upgrading my Snappymail. The
|
||||||
|
# commands should be used at your own risk. I take no responsibility.
|
||||||
|
#
|
||||||
|
# The script can only be run from the SnappyMail install directory!
|
||||||
|
#
|
||||||
|
# By Uwe Bieling <pychi@gmx.de>
|
||||||
|
# and Jordan S (https://github.com/jas8522)
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
LATEST_URL="https://snappymail.eu/repository/latest.tar.gz"
|
||||||
|
cd "${SCRIPT_DIR}"
|
||||||
|
OWNERGROUP=`stat -c "%U:%G" snappymail`
|
||||||
|
OLD_VERSION=`grep "define('APP_VERSION" index.php | awk -F\' '{print $4}'`
|
||||||
|
|
||||||
|
# check if we are root
|
||||||
|
if [[ $(id -u) -ne 0 ]] ; then echo -e "\033[1;31mPlease run as root\033[0m" ; exit 1 ; fi
|
||||||
|
|
||||||
|
# check if we are in the correct dir
|
||||||
|
if [ ! -d snappymail/v ] ; then echo -e "\033[1;31mThis script can only be run from the SnappyMail install directory\033[0m" ; exit 1 ; fi
|
||||||
|
|
||||||
|
# Download last release to /tmp
|
||||||
|
echo -e "\033[1;33mDownloading last release\033[0m"
|
||||||
|
wget $LATEST_URL -O /tmp/snappymail_latest.tar.gz >/dev/null 2>&1
|
||||||
|
NEW_VERSION=`tar -tf /tmp/snappymail_latest.tar.gz | grep "snappymail/v/.*/index.php" | awk -F/ '{print $3}'`
|
||||||
|
|
||||||
|
if [ $OLD_VERSION != $NEW_VERSION ] ;
|
||||||
|
then
|
||||||
|
# Safety First ... make a backup
|
||||||
|
DSTAMP=`date +%Y-%m-%d`
|
||||||
|
echo -e "\033[1;33mBacking up snappymail $OLD_VERSION to ../backup_snappymail_${DSTAMP}.tar.gz\033[0m"
|
||||||
|
tar -czf ../backup_snappymail_${DSTAMP}.tar.gz .
|
||||||
|
|
||||||
|
# installing
|
||||||
|
echo -e "\033[1;33mInstalling $NEW_VERSION\033[0m";
|
||||||
|
tar -xzf /tmp/snappymail_latest.tar.gz;
|
||||||
|
|
||||||
|
# set permissions
|
||||||
|
echo -e "\033[1;33mSet permissions\033[0m"
|
||||||
|
find . -type d -exec chmod 755 {} \;
|
||||||
|
find . -type f -exec chmod 644 {} \;
|
||||||
|
chmod u+x "${SCRIPT_DIR}/upgrade.sh"
|
||||||
|
chown -R $OWNERGROUP $SCRIPT_DIR
|
||||||
|
echo -e "\033[1;32mFinished with snappymail upgrade from $OLD_VERSION to $NEW_VERSION... \033[0m"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo -e "\033[1;31msame Version skipping.. \033[0m";
|
||||||
|
fi
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
|
||||||
import { changeTheme } from 'Common/Utils';
|
|
||||||
|
|
||||||
import { logoutLink } from 'Common/Links';
|
import { logoutLink } from 'Common/Links';
|
||||||
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
||||||
|
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { arePopupsVisible } from 'Knoin/Knoin';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
|
||||||
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
import { InputComponent } from 'Component/Input';
|
|
||||||
import { SelectComponent } from 'Component/Select';
|
import { SelectComponent } from 'Component/Select';
|
||||||
import { TextAreaComponent } from 'Component/TextArea';
|
|
||||||
import { CheckboxMaterialDesignComponent } from 'Component/MaterialDesign/Checkbox';
|
|
||||||
import { CheckboxComponent } from 'Component/Checkbox';
|
import { CheckboxComponent } from 'Component/Checkbox';
|
||||||
|
|
||||||
export class AbstractApp {
|
export class AbstractApp {
|
||||||
|
|
@ -23,58 +19,37 @@ export class AbstractApp {
|
||||||
this.Remote = Remote;
|
this.Remote = Remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutReload() {
|
logoutReload(url) {
|
||||||
const url = logoutLink();
|
arePopupsVisible(false);
|
||||||
|
url = url || logoutLink();
|
||||||
if (location.href !== url) {
|
if (location.href !== url) {
|
||||||
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 100);
|
setTimeout(() => location.href = url, 100);
|
||||||
} else {
|
} else {
|
||||||
rl.route.reload();
|
rl.route.reload();
|
||||||
}
|
}
|
||||||
}
|
// this does not work due to ViewModelClass.__builded = true;
|
||||||
|
// rl.settings.set('Auth', false);
|
||||||
refresh() {
|
// rl.app.start();
|
||||||
// rl.adminArea() || !translatorReload(false, );
|
|
||||||
rl.adminArea() || (
|
|
||||||
LanguageStore.language(SettingsGet('Language'))
|
|
||||||
& ThemeStore.populate()
|
|
||||||
& changeTheme(SettingsGet('Theme'))
|
|
||||||
);
|
|
||||||
|
|
||||||
this.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstart() {
|
bootstart() {
|
||||||
const register = (key, ClassObject, templateID) => ko.components.register(key, {
|
const register = (name, ClassObject) => ko.components.register(name, {
|
||||||
template: { element: templateID || (key + 'Component') },
|
template: { element: ClassObject.name },
|
||||||
viewModel: {
|
viewModel: {
|
||||||
createViewModel: (params, componentInfo) => {
|
createViewModel: (params, componentInfo) => {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
|
i18nToNodes(componentInfo.element);
|
||||||
if (componentInfo && componentInfo.element) {
|
|
||||||
|
|
||||||
i18nToNodes(componentInfo.element);
|
|
||||||
|
|
||||||
if (params.inline) {
|
|
||||||
componentInfo.element.style.display = 'inline-block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ClassObject(params);
|
return new ClassObject(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
register('Input', InputComponent);
|
|
||||||
register('Select', SelectComponent);
|
register('Select', SelectComponent);
|
||||||
register('TextArea', TextAreaComponent);
|
register('Checkbox', CheckboxComponent);
|
||||||
register('Checkbox', CheckboxMaterialDesignComponent, 'CheckboxMaterialDesignComponent');
|
|
||||||
register('CheckboxSimple', CheckboxComponent, 'CheckboxComponent');
|
|
||||||
|
|
||||||
initOnStartOrLangChange();
|
initOnStartOrLangChange();
|
||||||
|
|
||||||
LanguageStore.populate();
|
LanguageStore.populate();
|
||||||
ThemeStore.populate();
|
initThemes();
|
||||||
|
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
|
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { SettingsGet, SettingsAdmin } from 'Common/Globals';
|
||||||
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
|
|
@ -10,18 +11,26 @@ import { LoginAdminScreen } from 'Screen/Admin/Login';
|
||||||
import { startScreens } from 'Knoin/Knoin';
|
import { startScreens } from 'Knoin/Knoin';
|
||||||
import { AbstractApp } from 'App/Abstract';
|
import { AbstractApp } from 'App/Abstract';
|
||||||
|
|
||||||
class AdminApp extends AbstractApp {
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
|
export class AdminApp extends AbstractApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Remote);
|
super(Remote);
|
||||||
this.weakPassword = ko.observable(false);
|
this.weakPassword = ko.observable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
initThemes();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (!Settings.app('adminAllowed')) {
|
// if (!Settings.app('adminAllowed')) {
|
||||||
|
if (!SettingsAdmin('allowed')) {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.href = '/', 1);
|
setTimeout(() => location.href = '/', 1);
|
||||||
} else if (SettingsGet('Auth')) {
|
} else if (SettingsGet('Auth')) {
|
||||||
this.weakPassword(!!SettingsGet('WeakPassword'));
|
this.weakPassword(SettingsGet('weakPassword'));
|
||||||
startScreens([SettingsAdminScreen]);
|
startScreens([SettingsAdminScreen]);
|
||||||
} else {
|
} else {
|
||||||
startScreens([LoginAdminScreen]);
|
startScreens([LoginAdminScreen]);
|
||||||
|
|
@ -29,4 +38,15 @@ class AdminApp extends AbstractApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AdminApp();
|
AskPopupView.credentials = function(sAskDesc, btnText) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.showModal([
|
||||||
|
sAskDesc,
|
||||||
|
view => resolve({username:view.username(), password:view.passphrase()}),
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
3,
|
||||||
|
btnText
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
653
dev/App/User.js
653
dev/App/User.js
|
|
@ -1,18 +1,12 @@
|
||||||
import 'External/User/ko';
|
import 'External/User/ko';
|
||||||
|
|
||||||
import { isArray, arrayLength, pString, forEachObjectValue } from 'Common/Utils';
|
import { SMAudio } from 'Common/Audio';
|
||||||
import { delegateRunOnDestroy, mailToHelper, setLayoutResizer } from 'Common/UtilsUser';
|
import { arrayLength } from 'Common/Utils';
|
||||||
|
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
|
||||||
import {
|
|
||||||
Notification,
|
|
||||||
Scope
|
|
||||||
} from 'Common/Enums';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FolderType,
|
FolderType,
|
||||||
SetSystemFoldersNotification,
|
ClientSideKeyNameFolderListSize
|
||||||
MessageSetAction,
|
|
||||||
ClientSideKeyName
|
|
||||||
} from 'Common/EnumsUser';
|
} from 'Common/EnumsUser';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -21,39 +15,34 @@ import {
|
||||||
$htmlCL,
|
$htmlCL,
|
||||||
Settings,
|
Settings,
|
||||||
SettingsGet,
|
SettingsGet,
|
||||||
leftPanelDisabled
|
leftPanelDisabled,
|
||||||
|
addEventsListener,
|
||||||
|
addShortcut
|
||||||
} from 'Common/Globals';
|
} from 'Common/Globals';
|
||||||
|
|
||||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MessageFlagsCache,
|
|
||||||
setFolderHash,
|
|
||||||
getFolderHash,
|
|
||||||
getFolderInboxName,
|
getFolderInboxName,
|
||||||
getFolderFromCacheList
|
getFolderFromCacheList
|
||||||
} from 'Common/Cache';
|
} from 'Common/Cache';
|
||||||
|
|
||||||
import { mailBox } from 'Common/Links';
|
import { i18n, reloadTime, getErrorMessage } from 'Common/Translator';
|
||||||
|
|
||||||
import { getNotification, i18n } from 'Common/Translator';
|
|
||||||
|
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
import { ContactUserStore } from 'Stores/User/Contact';
|
import { ContactUserStore } from 'Stores/User/Contact';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||||
|
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||||
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
import { ThemeStore, initThemes } from 'Stores/Theme';
|
||||||
|
import { LanguageStore } from 'Stores/Language';
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { EmailModel } from 'Model/Email';
|
|
||||||
import { AccountModel } from 'Model/Account';
|
|
||||||
import { IdentityModel } from 'Model/Identity';
|
|
||||||
|
|
||||||
import { LoginUserScreen } from 'Screen/User/Login';
|
import { LoginUserScreen } from 'Screen/User/Login';
|
||||||
import { MailBoxUserScreen } from 'Screen/User/MailBox';
|
import { MailBoxUserScreen } from 'Screen/User/MailBox';
|
||||||
import { SettingsUserScreen } from 'Screen/User/Settings';
|
import { SettingsUserScreen } from 'Screen/User/Settings';
|
||||||
|
|
@ -66,610 +55,248 @@ import { ComposePopupView } from 'View/Popup/Compose';
|
||||||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||||
import { AskPopupView } from 'View/Popup/Ask';
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
import { timeToNode } from 'Common/Momentor';
|
import {
|
||||||
|
folderInformation,
|
||||||
|
folderInformationMultiply,
|
||||||
|
setRefreshFoldersInterval
|
||||||
|
} from 'Common/Folders';
|
||||||
|
import { loadFolders } from 'Model/FolderCollection';
|
||||||
|
|
||||||
// Every 5 minutes
|
export class AppUser extends AbstractApp {
|
||||||
const refreshFolders = 300000;
|
|
||||||
|
|
||||||
let moveCache = {};
|
|
||||||
|
|
||||||
class AppUser extends AbstractApp {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Remote);
|
super(Remote);
|
||||||
|
|
||||||
this.moveOrDeleteResponseHelper = this.moveOrDeleteResponseHelper.bind(this);
|
|
||||||
|
|
||||||
this.messagesMoveTrigger = this.messagesMoveTrigger.debounce(500);
|
|
||||||
|
|
||||||
// wakeUp
|
// wakeUp
|
||||||
const interval = 3600000; // 60m
|
const interval = 3600000; // 60m
|
||||||
var lastTime = Date.now();
|
let lastTime = Date.now();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
if (currentTime > (lastTime + interval + 1000)) {
|
(currentTime > (lastTime + interval + 1000))
|
||||||
Remote.request('Version',
|
&& Remote.request('Version',
|
||||||
iError => (100 < iError) && this.reload(),
|
iError => (100 < iError) && location.reload(),
|
||||||
{ Version: Settings.app('version') }
|
{ version: Settings.app('version') }
|
||||||
);
|
);
|
||||||
}
|
|
||||||
lastTime = currentTime;
|
lastTime = currentTime;
|
||||||
}, interval);
|
}, interval);
|
||||||
|
|
||||||
const fn = (ev=>$htmlCL.toggle('rl-ctrl-key-pressed', ev.ctrlKey)).debounce(500);
|
addEventsListener(doc, ['keydown','keyup'], (ev=>$htmlCL.toggle('rl-ctrl-key-pressed', ev.ctrlKey)).debounce(500));
|
||||||
['keydown','keyup'].forEach(t => doc.addEventListener(t, fn));
|
|
||||||
|
|
||||||
shortcuts.add('escape,enter', '', Scope.All, () => rl.Dropdowns.detectVisibility());
|
addShortcut('escape,enter', '', dropdownsDetectVisibility);
|
||||||
}
|
addEventListener('click', dropdownsDetectVisibility);
|
||||||
|
|
||||||
reload() {
|
this.folderList = FolderUserStore.folderList;
|
||||||
(Settings.app('inIframe') ? parent : window).location.reload();
|
this.messageList = MessagelistUserStore;
|
||||||
|
|
||||||
|
this.ask = AskPopupView;
|
||||||
|
|
||||||
|
this.loadAccountsAndIdentities = loadAccountsAndIdentities;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} bDropPagePosition = false
|
* @param {number} iFolderType
|
||||||
* @param {boolean=} bDropCurrenFolderCache = false
|
|
||||||
*/
|
|
||||||
reloadMessageList(bDropPagePosition = false, bDropCurrenFolderCache = false) {
|
|
||||||
let iOffset = (MessageUserStore.listPage() - 1) * SettingsUserStore.messagesPerPage();
|
|
||||||
|
|
||||||
if (bDropCurrenFolderCache) {
|
|
||||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bDropPagePosition) {
|
|
||||||
MessageUserStore.listPage(1);
|
|
||||||
MessageUserStore.listPageBeforeThread(1);
|
|
||||||
iOffset = 0;
|
|
||||||
|
|
||||||
rl.route.setHash(
|
|
||||||
mailBox(
|
|
||||||
FolderUserStore.currentFolderFullNameHash(),
|
|
||||||
MessageUserStore.listPage(),
|
|
||||||
MessageUserStore.listSearch(),
|
|
||||||
MessageUserStore.listThreadUid()
|
|
||||||
),
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUserStore.listLoading(true);
|
|
||||||
MessageUserStore.listError('');
|
|
||||||
Remote.messageList(
|
|
||||||
(iError, oData, bCached) => {
|
|
||||||
if (iError) {
|
|
||||||
if (Notification.RequestAborted !== iError) {
|
|
||||||
MessageUserStore.list([]);
|
|
||||||
MessageUserStore.listError(getNotification(iError));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MessageUserStore.setMessageList(oData, bCached);
|
|
||||||
}
|
|
||||||
MessageUserStore.listLoading(false);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Folder: FolderUserStore.currentFolderFullName(),
|
|
||||||
Offset: iOffset,
|
|
||||||
Limit: SettingsUserStore.messagesPerPage(),
|
|
||||||
Search: MessageUserStore.listSearch(),
|
|
||||||
ThreadUid: MessageUserStore.listThreadUid()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesMoveTrigger() {
|
|
||||||
const sTrashFolder = FolderUserStore.trashFolder(),
|
|
||||||
sSpamFolder = FolderUserStore.spamFolder();
|
|
||||||
|
|
||||||
forEachObjectValue(moveCache, item => {
|
|
||||||
const isSpam = sSpamFolder === item.To,
|
|
||||||
isTrash = sTrashFolder === item.To,
|
|
||||||
isHam = !isSpam && sSpamFolder === item.From && getFolderInboxName() === item.To;
|
|
||||||
|
|
||||||
Remote.request('MessageMove',
|
|
||||||
this.moveOrDeleteResponseHelper,
|
|
||||||
{
|
|
||||||
FromFolder: item.From,
|
|
||||||
ToFolder: item.To,
|
|
||||||
Uids: item.Uid.join(','),
|
|
||||||
MarkAsRead: (isSpam || isTrash) ? 1 : 0,
|
|
||||||
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
['MessageList']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
moveCache = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesMoveHelper(fromFolderFullName, toFolderFullName, uidsForMove) {
|
|
||||||
const hash = '$$' + fromFolderFullName + '$$' + toFolderFullName + '$$';
|
|
||||||
if (!moveCache[hash]) {
|
|
||||||
moveCache[hash] = {
|
|
||||||
From: fromFolderFullName,
|
|
||||||
To: toFolderFullName,
|
|
||||||
Uid: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
moveCache[hash].Uid = moveCache[hash].Uid.concat(uidsForMove).unique();
|
|
||||||
this.messagesMoveTrigger();
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesDeleteHelper(sFromFolderFullName, aUidForRemove) {
|
|
||||||
Remote.request('MessageDelete',
|
|
||||||
this.moveOrDeleteResponseHelper,
|
|
||||||
{
|
|
||||||
Folder: sFromFolderFullName,
|
|
||||||
Uids: aUidForRemove.join(',')
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
['MessageList']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveOrDeleteResponseHelper(iError, oData) {
|
|
||||||
if (iError) {
|
|
||||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
|
||||||
alert(getNotification(iError));
|
|
||||||
} else if (FolderUserStore.currentFolder()) {
|
|
||||||
if (2 === arrayLength(oData.Result)) {
|
|
||||||
setFolderHash(oData.Result[0], oData.Result[1]);
|
|
||||||
} else {
|
|
||||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
|
||||||
}
|
|
||||||
this.reloadMessageList(!MessageUserStore.list.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} iDeleteType
|
|
||||||
* @param {string} sFromFolderFullName
|
* @param {string} sFromFolderFullName
|
||||||
* @param {Array} aUidForRemove
|
* @param {Set} oUids
|
||||||
* @param {boolean=} bUseFolder = true
|
* @param {boolean=} bDelete = false
|
||||||
*/
|
*/
|
||||||
deleteMessagesFromFolder(iDeleteType, sFromFolderFullName, aUidForRemove, bUseFolder) {
|
moveMessagesToFolderType(iFolderType, sFromFolderFullName, oUids, bDelete) {
|
||||||
let oMoveFolder = null,
|
let oMoveFolder = null,
|
||||||
nSetSystemFoldersNotification = null;
|
nSetSystemFoldersNotification = 0;
|
||||||
|
|
||||||
switch (iDeleteType) {
|
switch (iFolderType) {
|
||||||
case FolderType.Spam:
|
case FolderType.Junk:
|
||||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.spamFolder());
|
oMoveFolder = getFolderFromCacheList(FolderUserStore.spamFolder());
|
||||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Spam;
|
nSetSystemFoldersNotification = iFolderType;
|
||||||
|
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.spamFolder();
|
||||||
break;
|
break;
|
||||||
case FolderType.NotSpam:
|
case FolderType.Inbox:
|
||||||
oMoveFolder = getFolderFromCacheList(getFolderInboxName());
|
oMoveFolder = getFolderFromCacheList(getFolderInboxName());
|
||||||
break;
|
break;
|
||||||
case FolderType.Trash:
|
case FolderType.Trash:
|
||||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
|
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
|
||||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Trash;
|
nSetSystemFoldersNotification = iFolderType;
|
||||||
|
bDelete = bDelete/* || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()*/
|
||||||
|
|| sFromFolderFullName === FolderUserStore.spamFolder()
|
||||||
|
|| sFromFolderFullName === FolderUserStore.trashFolder();
|
||||||
break;
|
break;
|
||||||
case FolderType.Archive:
|
case FolderType.Archive:
|
||||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.archiveFolder());
|
oMoveFolder = getFolderFromCacheList(FolderUserStore.archiveFolder());
|
||||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Archive;
|
nSetSystemFoldersNotification = iFolderType;
|
||||||
|
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.archiveFolder();
|
||||||
break;
|
break;
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
|
|
||||||
bUseFolder = undefined === bUseFolder ? true : !!bUseFolder;
|
if (bDelete) {
|
||||||
if (bUseFolder) {
|
|
||||||
if (
|
|
||||||
(FolderType.Spam === iDeleteType && UNUSED_OPTION_VALUE === FolderUserStore.spamFolder()) ||
|
|
||||||
(FolderType.Trash === iDeleteType && UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()) ||
|
|
||||||
(FolderType.Archive === iDeleteType && UNUSED_OPTION_VALUE === FolderUserStore.archiveFolder())
|
|
||||||
) {
|
|
||||||
bUseFolder = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oMoveFolder && bUseFolder) {
|
|
||||||
showScreenPopup(FolderSystemPopupView, [nSetSystemFoldersNotification]);
|
|
||||||
} else if (
|
|
||||||
!bUseFolder ||
|
|
||||||
(FolderType.Trash === iDeleteType &&
|
|
||||||
(sFromFolderFullName === FolderUserStore.spamFolder()
|
|
||||||
|| sFromFolderFullName === FolderUserStore.trashFolder()))
|
|
||||||
) {
|
|
||||||
showScreenPopup(AskPopupView, [
|
showScreenPopup(AskPopupView, [
|
||||||
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
|
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
|
||||||
() => {
|
() => {
|
||||||
this.messagesDeleteHelper(sFromFolderFullName, aUidForRemove);
|
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids);
|
||||||
MessageUserStore.removeMessagesFromList(sFromFolderFullName, aUidForRemove);
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
} else if (oMoveFolder) {
|
} else if (oMoveFolder) {
|
||||||
this.messagesMoveHelper(sFromFolderFullName, oMoveFolder.fullName, aUidForRemove);
|
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids, oMoveFolder.fullName);
|
||||||
MessageUserStore.removeMessagesFromList(sFromFolderFullName, aUidForRemove, oMoveFolder.fullName);
|
} else {
|
||||||
|
showScreenPopup(FolderSystemPopupView, [nSetSystemFoldersNotification]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} sFromFolderFullName
|
|
||||||
* @param {Array} aUidForMove
|
|
||||||
* @param {string} sToFolderFullName
|
|
||||||
* @param {boolean=} bCopy = false
|
|
||||||
*/
|
|
||||||
moveMessagesToFolder(sFromFolderFullName, aUidForMove, sToFolderFullName, bCopy) {
|
|
||||||
if (sFromFolderFullName !== sToFolderFullName && arrayLength(aUidForMove)) {
|
|
||||||
const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
|
|
||||||
oToFolder = getFolderFromCacheList(sToFolderFullName);
|
|
||||||
|
|
||||||
if (oFromFolder && oToFolder) {
|
|
||||||
if (undefined === bCopy ? false : !!bCopy) {
|
|
||||||
Remote.request('MessageCopy', null, {
|
|
||||||
FromFolder: oFromFolder.fullName,
|
|
||||||
ToFolder: oToFolder.fullName,
|
|
||||||
Uids: aUidForMove.join(',')
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, aUidForMove);
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUserStore.removeMessagesFromList(oFromFolder.fullName, aUidForMove, oToFolder.fullName, bCopy);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
accountsAndIdentities() {
|
|
||||||
AccountUserStore.loading(true);
|
|
||||||
IdentityUserStore.loading(true);
|
|
||||||
|
|
||||||
Remote.request('AccountsAndIdentities', (iError, oData) => {
|
|
||||||
AccountUserStore.loading(false);
|
|
||||||
IdentityUserStore.loading(false);
|
|
||||||
|
|
||||||
if (!iError) {
|
|
||||||
const
|
|
||||||
// counts = {},
|
|
||||||
accounts = oData.Result.Accounts,
|
|
||||||
mainEmail = SettingsGet('MainEmail');
|
|
||||||
|
|
||||||
if (isArray(accounts)) {
|
|
||||||
// AccountUserStore.accounts.forEach(oAccount => counts[oAccount.email] = oAccount.count());
|
|
||||||
|
|
||||||
delegateRunOnDestroy(AccountUserStore.accounts());
|
|
||||||
|
|
||||||
AccountUserStore.accounts(
|
|
||||||
accounts.map(
|
|
||||||
sValue => new AccountModel(sValue/*, counts[sValue]*/)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
// accounts.length &&
|
|
||||||
AccountUserStore.accounts.unshift(new AccountModel(mainEmail/*, counts[mainEmail]*/, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray(oData.Result.Identities)) {
|
|
||||||
delegateRunOnDestroy(IdentityUserStore());
|
|
||||||
|
|
||||||
IdentityUserStore(
|
|
||||||
oData.Result.Identities.map(identityData => {
|
|
||||||
const identity = new IdentityModel(
|
|
||||||
pString(identityData.Id),
|
|
||||||
pString(identityData.Email)
|
|
||||||
);
|
|
||||||
|
|
||||||
identity.name(pString(identityData.Name));
|
|
||||||
identity.replyTo(pString(identityData.ReplyTo));
|
|
||||||
identity.bcc(pString(identityData.Bcc));
|
|
||||||
identity.signature(pString(identityData.Signature));
|
|
||||||
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
|
|
||||||
|
|
||||||
return identity;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folder
|
* @param {string} folder
|
||||||
* @param {Array=} list = []
|
* @param {Array=} list = []
|
||||||
*/
|
*/
|
||||||
folderInformation(folder, list) {
|
folderInformation(folder, list) {
|
||||||
if (folder && folder.trim()) {
|
folderInformation(folder, list);
|
||||||
Remote.folderInformation(
|
|
||||||
(iError, data) => {
|
|
||||||
if (!iError && data.Result) {
|
|
||||||
const result = data.Result,
|
|
||||||
hash = getFolderHash(result.Folder),
|
|
||||||
folderFromCache = getFolderFromCacheList(result.Folder);
|
|
||||||
if (folderFromCache) {
|
|
||||||
folderFromCache.expires = Date.now();
|
|
||||||
|
|
||||||
setFolderHash(result.Folder, result.Hash);
|
|
||||||
|
|
||||||
folderFromCache.messageCountAll(result.MessageCount);
|
|
||||||
|
|
||||||
let unreadCountChange = (folderFromCache.messageCountUnread() !== result.MessageUnseenCount);
|
|
||||||
|
|
||||||
folderFromCache.messageCountUnread(result.MessageUnseenCount);
|
|
||||||
|
|
||||||
if (unreadCountChange) {
|
|
||||||
MessageFlagsCache.clearFolder(folderFromCache.fullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Flags.length) {
|
|
||||||
result.Flags.forEach(message =>
|
|
||||||
MessageFlagsCache.setFor(folderFromCache.fullName, message.Uid.toString(), message.Flags)
|
|
||||||
);
|
|
||||||
|
|
||||||
MessageUserStore.reloadFlagsAndCachedMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUserStore.initUidNextAndNewMessages(
|
|
||||||
folderFromCache.fullName,
|
|
||||||
result.UidNext,
|
|
||||||
result.NewMessages
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hash || unreadCountChange || result.Hash !== hash) {
|
|
||||||
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
|
||||||
this.reloadMessageList();
|
|
||||||
} else if (getFolderInboxName() === folderFromCache.fullName) {
|
|
||||||
Remote.messageList(null, {Folder: getFolderInboxName()}, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
folder,
|
|
||||||
list
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean=} boot = false
|
|
||||||
*/
|
|
||||||
folderInformationMultiply(boot = false) {
|
|
||||||
const folders = FolderUserStore.getNextFolderNames(refreshFolders);
|
|
||||||
if (arrayLength(folders)) {
|
|
||||||
Remote.request('FolderInformationMultiply', (iError, oData) => {
|
|
||||||
if (!iError && arrayLength(oData.Result)) {
|
|
||||||
const utc = Date.now();
|
|
||||||
oData.Result.forEach(item => {
|
|
||||||
const hash = getFolderHash(item.Folder),
|
|
||||||
folder = getFolderFromCacheList(item.Folder);
|
|
||||||
|
|
||||||
if (folder) {
|
|
||||||
folder.expires = utc;
|
|
||||||
|
|
||||||
setFolderHash(item.Folder, item.Hash);
|
|
||||||
|
|
||||||
folder.messageCountAll(item.MessageCount);
|
|
||||||
|
|
||||||
let unreadCountChange = folder.messageCountUnread() !== item.MessageUnseenCount;
|
|
||||||
|
|
||||||
folder.messageCountUnread(item.MessageUnseenCount);
|
|
||||||
|
|
||||||
if (unreadCountChange) {
|
|
||||||
MessageFlagsCache.clearFolder(folder.fullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hash || item.Hash !== hash) {
|
|
||||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
|
||||||
this.reloadMessageList();
|
|
||||||
}
|
|
||||||
} else if (unreadCountChange
|
|
||||||
&& folder.fullName === FolderUserStore.currentFolderFullName()
|
|
||||||
&& MessageUserStore.list.length) {
|
|
||||||
this.folderInformation(folder.fullName, MessageUserStore.list());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (boot) {
|
|
||||||
setTimeout(() => this.folderInformationMultiply(true), 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
Folders: folders
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} sFolderFullName
|
|
||||||
* @param {number} iSetAction
|
|
||||||
* @param {Array=} messages = null
|
|
||||||
*/
|
|
||||||
messageListAction(sFolderFullName, iSetAction, messages) {
|
|
||||||
messages = messages || MessageUserStore.listChecked();
|
|
||||||
|
|
||||||
let folder = null,
|
|
||||||
alreadyUnread = 0,
|
|
||||||
rootUids = messages.map(oMessage => oMessage && oMessage.uid ? oMessage.uid : null)
|
|
||||||
.validUnique(),
|
|
||||||
length = rootUids.length;
|
|
||||||
|
|
||||||
if (sFolderFullName && length) {
|
|
||||||
switch (iSetAction) {
|
|
||||||
case MessageSetAction.SetSeen:
|
|
||||||
length = 0;
|
|
||||||
// fallthrough is intentionally
|
|
||||||
case MessageSetAction.UnsetSeen:
|
|
||||||
rootUids.forEach(sSubUid =>
|
|
||||||
alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
|
|
||||||
);
|
|
||||||
|
|
||||||
folder = getFolderFromCacheList(sFolderFullName);
|
|
||||||
if (folder) {
|
|
||||||
folder.messageCountUnread(folder.messageCountUnread() - alreadyUnread + length);
|
|
||||||
}
|
|
||||||
|
|
||||||
Remote.request('MessageSetSeen', null, {
|
|
||||||
Folder: sFolderFullName,
|
|
||||||
Uids: rootUids.join(','),
|
|
||||||
SetAction: iSetAction == MessageSetAction.SetSeen ? 1 : 0
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MessageSetAction.SetFlag:
|
|
||||||
case MessageSetAction.UnsetFlag:
|
|
||||||
rootUids.forEach(sSubUid =>
|
|
||||||
MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
|
|
||||||
);
|
|
||||||
Remote.request('MessageSetFlagged', null, {
|
|
||||||
Folder: sFolderFullName,
|
|
||||||
Uids: rootUids.join(','),
|
|
||||||
SetAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUserStore.reloadFlagsAndCachedMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} query
|
|
||||||
* @param {Function} autocompleteCallback
|
|
||||||
*/
|
|
||||||
getAutocomplete(query, autocompleteCallback) {
|
|
||||||
Remote.suggestions((iError, data) => {
|
|
||||||
if (!iError && isArray(data.Result)) {
|
|
||||||
autocompleteCallback(
|
|
||||||
data.Result.map(item => (item && item[0] ? new EmailModel(item[0], item[1]) : null)).filter(v => v)
|
|
||||||
);
|
|
||||||
} else if (Notification.RequestAborted !== iError) {
|
|
||||||
autocompleteCallback([]);
|
|
||||||
}
|
|
||||||
}, query);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
Remote.request('Logout', () => rl.logoutReload());
|
Remote.request('Logout', (iError, data) =>
|
||||||
|
iError ? alert('Logout error: ' + getErrorMessage(iError, data))
|
||||||
|
: rl.logoutReload(Settings.app('customLogoutLink'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstart() {
|
bootstart() {
|
||||||
super.bootstart();
|
super.bootstart();
|
||||||
|
|
||||||
addEventListener('resize', () => leftPanelDisabled(ThemeStore.isMobile() || 1000 > innerWidth));
|
|
||||||
addEventListener('beforeunload', event => {
|
addEventListener('beforeunload', event => {
|
||||||
if (arePopupsVisible()) {
|
if (arePopupsVisible() || (!SettingsUserStore.usePreviewPane() && MessageUserStore.message())) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return event.returnValue = "Are you sure you want to exit?";
|
return event.returnValue = i18n('POPUPS_ASK/EXIT_ARE_YOU_SURE');
|
||||||
}
|
}
|
||||||
}, {capture: true});
|
}, {capture: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
initThemes();
|
||||||
|
LanguageStore.language(SettingsGet('language'));
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (SettingsGet('Auth')) {
|
if (SettingsGet('Auth')) {
|
||||||
rl.setWindowTitle(i18n('GLOBAL/LOADING'));
|
rl.setTitle(i18n('GLOBAL/LOADING'));
|
||||||
|
|
||||||
NotificationUserStore.enableSoundNotification(!!SettingsGet('SoundNotification'));
|
SMAudio.notifications(!!SettingsGet('SoundNotification'));
|
||||||
NotificationUserStore.enableDesktopNotification(!!SettingsGet('DesktopNotifications'));
|
NotificationUserStore.enabled(!!SettingsGet('DesktopNotifications'));
|
||||||
|
|
||||||
AccountUserStore.email(SettingsGet('Email'));
|
AccountUserStore.email(SettingsGet('Email'));
|
||||||
|
|
||||||
SettingsUserStore.init();
|
SettingsUserStore.init();
|
||||||
ContactUserStore.init();
|
ContactUserStore.init();
|
||||||
|
|
||||||
Remote.foldersReload(value => {
|
loadFolders((success, error) => {
|
||||||
try {
|
try {
|
||||||
if (value) {
|
if (success) {
|
||||||
startScreens([
|
startScreens([
|
||||||
MailBoxUserScreen,
|
MailBoxUserScreen,
|
||||||
SettingsUserScreen
|
SettingsUserScreen
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setInterval(() => {
|
setRefreshFoldersInterval(SettingsGet('CheckMailInterval'));
|
||||||
const cF = FolderUserStore.currentFolderFullName(),
|
|
||||||
iF = getFolderInboxName();
|
|
||||||
this.folderInformation(iF);
|
|
||||||
if (iF !== cF) {
|
|
||||||
this.folderInformation(cF);
|
|
||||||
}
|
|
||||||
this.folderInformationMultiply();
|
|
||||||
}, refreshFolders);
|
|
||||||
|
|
||||||
ContactUserStore.init();
|
loadAccountsAndIdentities();
|
||||||
|
|
||||||
this.accountsAndIdentities();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const cF = FolderUserStore.currentFolderFullName();
|
const cF = FolderUserStore.currentFolderFullName();
|
||||||
if (getFolderInboxName() !== cF) {
|
getFolderInboxName() === cF || folderInformation(cF);
|
||||||
this.folderInformation(cF);
|
FolderUserStore.hasCapability('LIST-STATUS') || folderInformationMultiply(true);
|
||||||
}
|
|
||||||
FolderUserStore.hasCapability('LIST-STATUS') || this.folderInformationMultiply(true);
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
setTimeout(() => Remote.request('AppDelayStart'), 35000);
|
setTimeout(() => Remote.request('AppDelayStart'), 35000);
|
||||||
|
|
||||||
// When auto-login is active
|
|
||||||
if (
|
|
||||||
SettingsGet('AccountSignMe') &&
|
|
||||||
navigator.registerProtocolHandler
|
|
||||||
) {
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
navigator.registerProtocolHandler(
|
|
||||||
'mailto',
|
|
||||||
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
|
|
||||||
(SettingsGet('Title') || 'SnappyMail')
|
|
||||||
);
|
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
|
||||||
|
|
||||||
value = SettingsGet('MailToEmail');
|
|
||||||
value && mailToHelper(value);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add pointermove ?
|
// add pointermove ?
|
||||||
['touchstart','mousemove','keydown'].forEach(
|
addEventsListener(doc, ['touchstart','mousemove','keydown'], SettingsUserStore.delayLogout, {passive:true});
|
||||||
t => doc.addEventListener(t, SettingsUserStore.delayLogout, {passive:true})
|
|
||||||
);
|
|
||||||
SettingsUserStore.delayLogout();
|
SettingsUserStore.delayLogout();
|
||||||
|
|
||||||
// initLeftSideLayoutResizer
|
// initLeftSideLayoutResizer
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const left = elementById('rl-left'),
|
const left = elementById('rl-left'),
|
||||||
right = elementById('rl-right'),
|
|
||||||
fToggle = () =>
|
fToggle = () =>
|
||||||
setLayoutResizer(left, right, ClientSideKeyName.FolderListSize,
|
setLayoutResizer(left, ClientSideKeyNameFolderListSize,
|
||||||
(ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
|
(ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
|
||||||
if (left && right) {
|
if (left) {
|
||||||
fToggle();
|
fToggle();
|
||||||
leftPanelDisabled.subscribe(fToggle);
|
leftPanelDisabled.subscribe(fToggle);
|
||||||
}
|
}
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
setInterval(this.reloadTime(), 60000);
|
setInterval(reloadTime, 60000);
|
||||||
|
|
||||||
PgpUserStore.init();
|
PgpUserStore.init();
|
||||||
|
SMimeUserStore.loadCertificates();
|
||||||
|
|
||||||
|
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
||||||
} else {
|
} else {
|
||||||
this.logout();
|
this.logout();
|
||||||
|
alert('Folders error: ' + getErrorMessage(0, error))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
startScreens([LoginUserScreen]);
|
startScreens([LoginUserScreen]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadTime()
|
|
||||||
{
|
|
||||||
setTimeout(() =>
|
|
||||||
doc.querySelectorAll('time').forEach(element => timeToNode(element))
|
|
||||||
, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessageComposer(params = [])
|
showMessageComposer(params = [])
|
||||||
{
|
{
|
||||||
showScreenPopup(ComposePopupView, params);
|
let msg = params[1];
|
||||||
|
if (1 == arrayLength(msg)) {
|
||||||
|
msg = msg[0];
|
||||||
|
}
|
||||||
|
if (msg) {
|
||||||
|
msg.decrypt().then((/*success*/)=>showScreenPopup(ComposePopupView, params));
|
||||||
|
} else {
|
||||||
|
showScreenPopup(ComposePopupView, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AppUser();
|
AskPopupView.password = function(sAskDesc, btnText, ask) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.showModal([
|
||||||
|
sAskDesc,
|
||||||
|
view => resolve({
|
||||||
|
password:view.passphrase(),
|
||||||
|
username:/*ask & 2 ? */view.username(),
|
||||||
|
remember:/*ask & 4 ? */view.remember(),
|
||||||
|
rememberPermanent: view.rememberPermanent(),
|
||||||
|
}),
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
ask || 1,
|
||||||
|
btnText
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
AskPopupView.cryptkey = () => new Promise(resolve => {
|
||||||
|
const fn = () => AskPopupView.showModal([
|
||||||
|
i18n('CRYPTO/ASK_CRYPTKEY_PASS'),
|
||||||
|
view => {
|
||||||
|
let pass = view.passphrase();
|
||||||
|
if (pass) {
|
||||||
|
Remote.post('ResealCryptKey', null, {
|
||||||
|
passphrase: pass
|
||||||
|
}).then(response => {
|
||||||
|
resolve(response?.Result);
|
||||||
|
}).catch(e => {
|
||||||
|
if (111 === e.code) {
|
||||||
|
fn();
|
||||||
|
} else {
|
||||||
|
console.error(e);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
1,
|
||||||
|
i18n('CRYPTO/DECRYPT')
|
||||||
|
]);
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import * as Links from 'Common/Links';
|
import * as Links from 'Common/Links';
|
||||||
import { doc, SettingsGet, fireEvent } from 'Common/Globals';
|
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
|
||||||
|
import { addObservablesTo } from 'External/ko';
|
||||||
|
|
||||||
let notificator = null,
|
let notificator = null,
|
||||||
player = null,
|
player = null,
|
||||||
canPlay = type => player && !!player.canPlayType(type).replace('no', ''),
|
canPlay = type => !!player?.canPlayType(type).replace('no', ''),
|
||||||
|
|
||||||
audioCtx = window.AudioContext || window.webkitAudioContext,
|
audioCtx = window.AudioContext || window.webkitAudioContext,
|
||||||
|
|
||||||
|
|
@ -45,12 +46,12 @@ let notificator = null,
|
||||||
'keydown','keyup'
|
'keydown','keyup'
|
||||||
],
|
],
|
||||||
unlock = () => {
|
unlock = () => {
|
||||||
|
unlockEvents.forEach(type => doc.removeEventListener(type, unlock, true));
|
||||||
if (audioCtx) {
|
if (audioCtx) {
|
||||||
console.log('AudioContext ' + audioCtx.state);
|
console.log('AudioContext ' + audioCtx.state);
|
||||||
audioCtx.resume();
|
audioCtx.resume();
|
||||||
}
|
}
|
||||||
unlockEvents.forEach(type => doc.removeEventListener(type, unlock, true));
|
// setTimeout(()=>SMAudio.playNotification(0,1),1);
|
||||||
// setTimeout(()=>Audio.playNotification(1),1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (audioCtx) {
|
if (audioCtx) {
|
||||||
|
|
@ -73,10 +74,13 @@ export const SMAudio = new class {
|
||||||
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
|
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
|
||||||
if (player) {
|
if (player) {
|
||||||
const stopFn = () => this.pause();
|
const stopFn = () => this.pause();
|
||||||
player.addEventListener('ended', stopFn);
|
addEventsListener(player, ['ended','error'], stopFn);
|
||||||
player.addEventListener('error', stopFn);
|
|
||||||
addEventListener('audio.api.stop', stopFn);
|
addEventListener('audio.api.stop', stopFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addObservablesTo(this, {
|
||||||
|
notifications: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
paused() {
|
paused() {
|
||||||
|
|
@ -88,7 +92,7 @@ export const SMAudio = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
player && player.pause();
|
player?.pause();
|
||||||
fireEvent('audio.stop');
|
fireEvent('audio.stop');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,18 +108,24 @@ export const SMAudio = new class {
|
||||||
this.supportedWav && play(url, name);
|
this.supportedWav && play(url, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
playNotification(silent) {
|
/**
|
||||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
* Used with SoundNotification setting
|
||||||
notificator = notificator || createNewObject();
|
*/
|
||||||
if (notificator) {
|
playNotification(force, silent) {
|
||||||
notificator.src = Links.staticLink('sounds/'
|
if (force || this.notifications()) {
|
||||||
+ SettingsGet('NotificationSound')
|
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||||
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
notificator = notificator || createNewObject();
|
||||||
notificator.volume = silent ? 0.01 : 1;
|
if (notificator) {
|
||||||
notificator.play();
|
// SettingsGet('NotificationSound').startsWith('custom@')
|
||||||
|
notificator.src = Links.staticLink('sounds/'
|
||||||
|
+ SettingsGet('NotificationSound')
|
||||||
|
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
||||||
|
notificator.volume = silent ? 0.01 : 1;
|
||||||
|
notificator.play();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No audio: ' + audioCtx.state);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log('No audio: ' + audioCtx.state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import { MessageSetAction } from 'Common/EnumsUser';
|
let FOLDERS_CACHE = new Map,
|
||||||
import { isArray } from 'Common/Utils';
|
FOLDERS_HASH_MAP = new Map,
|
||||||
|
|
||||||
let FOLDERS_CACHE = {},
|
|
||||||
FOLDERS_NAME_CACHE = {},
|
|
||||||
MESSAGE_FLAGS_CACHE = {},
|
|
||||||
NEW_MESSAGE_CACHE = {},
|
|
||||||
REQUESTED_MESSAGE_CACHE = {},
|
|
||||||
inboxFolderName = 'INBOX';
|
inboxFolderName = 'INBOX';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
@ -13,56 +7,10 @@ export const
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
clearCache = () => {
|
clearCache = () => {
|
||||||
FOLDERS_CACHE = {};
|
FOLDERS_CACHE.clear();
|
||||||
FOLDERS_NAME_CACHE = {};
|
FOLDERS_HASH_MAP.clear();
|
||||||
MESSAGE_FLAGS_CACHE = {};
|
|
||||||
NEW_MESSAGE_CACHE = {};
|
|
||||||
REQUESTED_MESSAGE_CACHE = {};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getMessageKey = (folderFullName, uid) => `${folderFullName}#${uid}`,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folder
|
|
||||||
* @param {string} uid
|
|
||||||
*/
|
|
||||||
addRequestedMessage = (folder, uid) => REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folder
|
|
||||||
* @param {string} uid
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
*/
|
|
||||||
addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
*/
|
|
||||||
hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
|
|
||||||
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
|
|
||||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
clearNewMessageCache = () => NEW_MESSAGE_CACHE = {},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -74,175 +22,35 @@ export const
|
||||||
setFolderInboxName = name => inboxFolderName = name,
|
setFolderInboxName = name => inboxFolderName = name,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderHash
|
* @param {string} fullNameHash
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getFolderFullName = folderHash =>
|
getFolderFromHashMap = fullNameHash => getFolderFromCacheList(FOLDERS_HASH_MAP.get(fullNameHash)),
|
||||||
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderHash
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {?FolderModel} folder
|
* @param {?FolderModel} folder
|
||||||
*/
|
*/
|
||||||
setFolder = folder => {
|
setFolder = folder => {
|
||||||
folder.hash = '';
|
folder.etag = '';
|
||||||
FOLDERS_CACHE[folder.fullName] = folder;
|
FOLDERS_CACHE.set(folder.fullName, folder);
|
||||||
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
|
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
* @returns {string}
|
* @param {string} folderETag
|
||||||
*/
|
*/
|
||||||
getFolderHash = folderFullName =>
|
setFolderETag = (folderFullName, folderETag) =>
|
||||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
|
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} folderHash
|
|
||||||
*/
|
|
||||||
setFolderHash = (folderFullName, folderHash) =>
|
|
||||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getFolderUidNext = folderFullName =>
|
|
||||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uidNext
|
|
||||||
*/
|
|
||||||
setFolderUidNext = (folderFullName, uidNext) =>
|
|
||||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
* @returns {?FolderModel}
|
* @returns {?FolderModel}
|
||||||
*/
|
*/
|
||||||
getFolderFromCacheList = folderFullName =>
|
getFolderFromCacheList = folderFullName =>
|
||||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
|
FOLDERS_CACHE.get(folderFullName),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
*/
|
*/
|
||||||
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
|
removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);
|
||||||
|
|
||||||
export class MessageFlagsCache
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {string} flag
|
|
||||||
* @returns {bool}
|
|
||||||
*/
|
|
||||||
static hasFlag(folderFullName, uid, flag) {
|
|
||||||
return MESSAGE_FLAGS_CACHE[folderFullName]
|
|
||||||
&& MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
|
||||||
&& MESSAGE_FLAGS_CACHE[folderFullName][uid].includes(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
* @returns {?Array}
|
|
||||||
*/
|
|
||||||
static getFor(folderFullName, uid) {
|
|
||||||
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {Array} flagsCache
|
|
||||||
*/
|
|
||||||
static setFor(folderFullName, uid, flags) {
|
|
||||||
if (isArray(flags)) {
|
|
||||||
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
|
||||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
|
||||||
}
|
|
||||||
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
*/
|
|
||||||
static clearFolder(folderFullName) {
|
|
||||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {(MessageModel|null)} message
|
|
||||||
*/
|
|
||||||
static initMessage(message) {
|
|
||||||
if (message) {
|
|
||||||
const uid = message.uid,
|
|
||||||
flags = this.getFor(message.folder, uid),
|
|
||||||
thread = message.threads;
|
|
||||||
|
|
||||||
if (isArray(flags)) {
|
|
||||||
message.flags(flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thread.length) {
|
|
||||||
const unseenSubUid = thread.find(iSubUid =>
|
|
||||||
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
|
|
||||||
);
|
|
||||||
|
|
||||||
const flaggedSubUid = thread.find(iSubUid =>
|
|
||||||
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
|
|
||||||
);
|
|
||||||
|
|
||||||
message.hasUnseenSubMessage(!!unseenSubUid);
|
|
||||||
message.hasFlaggedSubMessage(!!flaggedSubUid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {(MessageModel|null)} message
|
|
||||||
*/
|
|
||||||
static store(message) {
|
|
||||||
if (message) {
|
|
||||||
this.setFor(message.folder, message.uid, message.flags());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folder
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {number} setAction
|
|
||||||
*/
|
|
||||||
static storeBySetAction(folder, uid, setAction) {
|
|
||||||
let flags = this.getFor(folder, uid) || [];
|
|
||||||
const
|
|
||||||
unread = flags.includes('\\seen') ? 0 : 1,
|
|
||||||
add = item => flags.includes(item) || flags.push(item),
|
|
||||||
remove = item => flags = flags.filter(flag => flag != item);
|
|
||||||
|
|
||||||
switch (setAction) {
|
|
||||||
case MessageSetAction.SetSeen:
|
|
||||||
add('\\seen');
|
|
||||||
break;
|
|
||||||
case MessageSetAction.UnsetSeen:
|
|
||||||
remove('\\seen');
|
|
||||||
break;
|
|
||||||
case MessageSetAction.SetFlag:
|
|
||||||
add('\\flagged');
|
|
||||||
break;
|
|
||||||
case MessageSetAction.UnsetFlag:
|
|
||||||
remove('\\flagged');
|
|
||||||
break;
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setFor(folder, uid, flags);
|
|
||||||
|
|
||||||
return unread;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,19 @@
|
||||||
/* eslint quote-props: 0 */
|
/* eslint quote-props: 0 */
|
||||||
|
|
||||||
/**
|
export const
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
export const Capa = {
|
|
||||||
GnuPG: 'GNUPG',
|
|
||||||
OpenPGP: 'OPEN_PGP',
|
|
||||||
Prefetch: 'PREFETCH',
|
|
||||||
Contacts: 'CONTACTS',
|
|
||||||
Search: 'SEARCH',
|
|
||||||
SearchAdv: 'SEARCH_ADV',
|
|
||||||
MessageActions: 'MESSAGE_ACTIONS',
|
|
||||||
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
|
|
||||||
DangerousActions: 'DANGEROUS_ACTIONS',
|
|
||||||
Themes: 'THEMES',
|
|
||||||
UserBackground: 'USER_BACKGROUND',
|
|
||||||
Sieve: 'SIEVE',
|
|
||||||
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
|
|
||||||
AutoLogout: 'AUTOLOGOUT',
|
|
||||||
Kolab: 'KOLAB',
|
|
||||||
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
|
|
||||||
Identities: 'IDENTITIES'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const Scope = {
|
ScopeMessageList = 'MessageList',
|
||||||
All: 'all',
|
ScopeFolderList = 'FolderList',
|
||||||
None: 'none',
|
ScopeMessageView = 'MessageView',
|
||||||
Contacts: 'Contacts',
|
ScopeSettings = 'Settings',
|
||||||
MessageList: 'MessageList',
|
|
||||||
FolderList: 'FolderList',
|
|
||||||
MessageView: 'MessageView',
|
|
||||||
Compose: 'Compose',
|
|
||||||
Settings: 'Settings',
|
|
||||||
Menu: 'Menu',
|
|
||||||
OpenPgpKey: 'OpenPgpKey',
|
|
||||||
KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
|
|
||||||
Ask: 'Ask'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const UploadErrorCode = {
|
UploadErrorCode = {
|
||||||
Normal: 0,
|
Normal: 0,
|
||||||
FileIsTooBig: 1,
|
FileIsTooBig: 1,
|
||||||
FilePartiallyUploaded: 3,
|
FilePartiallyUploaded: 3,
|
||||||
|
|
@ -53,24 +22,25 @@ export const UploadErrorCode = {
|
||||||
OnSavingFile: 7,
|
OnSavingFile: 7,
|
||||||
FileType: 98,
|
FileType: 98,
|
||||||
Unknown: 99
|
Unknown: 99
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const SaveSettingsStep = {
|
SaveSettingStatus = {
|
||||||
Animate: -2,
|
Saving: -2,
|
||||||
Idle: -1,
|
Idle: -1,
|
||||||
TrueResult: 1,
|
Success: 1,
|
||||||
FalseResult: 0
|
Failed: 0
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const Notification = {
|
Notifications = {
|
||||||
RequestError: 1,
|
RequestError: 1,
|
||||||
RequestAborted: 2,
|
RequestAborted: 2,
|
||||||
|
RequestTimeout: 3,
|
||||||
|
|
||||||
// Global
|
// Global
|
||||||
InvalidToken: 101,
|
InvalidToken: 101,
|
||||||
|
|
@ -80,6 +50,7 @@ export const Notification = {
|
||||||
ConnectionError: 104,
|
ConnectionError: 104,
|
||||||
DomainNotAllowed: 109,
|
DomainNotAllowed: 109,
|
||||||
AccountNotAllowed: 110,
|
AccountNotAllowed: 110,
|
||||||
|
CryptKeyError: 111,
|
||||||
|
|
||||||
ContactsSyncError: 140,
|
ContactsSyncError: 140,
|
||||||
|
|
||||||
|
|
@ -125,7 +96,6 @@ export const Notification = {
|
||||||
JsonParse: 952,
|
JsonParse: 952,
|
||||||
// JsonTimeout: 953,
|
// JsonTimeout: 953,
|
||||||
|
|
||||||
UnknownNotification: 998,
|
|
||||||
UnknownError: 999,
|
UnknownError: 999,
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,33 @@
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const FolderType = {
|
export const FolderType = {
|
||||||
User: 0,
|
|
||||||
Inbox: 1,
|
Inbox: 1,
|
||||||
Sent: 2,
|
Sent: 2,
|
||||||
Drafts: 3,
|
Drafts: 3,
|
||||||
Spam: 4, // JUNK
|
Junk: 4, // Spam
|
||||||
Trash: 5,
|
Trash: 5,
|
||||||
Archive: 6,
|
Archive: 6
|
||||||
NotSpam: 80
|
/*
|
||||||
};
|
IMPORTANT : 10;
|
||||||
|
FLAGGED : 11;
|
||||||
|
ALL : 13;
|
||||||
|
// TODO: SnappyMail
|
||||||
|
TEMPLATES : 19;
|
||||||
|
// Kolab
|
||||||
|
CONFIGURATION : 20;
|
||||||
|
CALENDAR : 21;
|
||||||
|
CONTACTS : 22;
|
||||||
|
TASKS : 23;
|
||||||
|
NOTES : 24;
|
||||||
|
FILES : 25;
|
||||||
|
JOURNAL : 26;
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const FolderMetadataKeys = {
|
FolderMetadataKeys = {
|
||||||
// RFC 5464
|
// RFC 5464
|
||||||
Comment: '/private/comment',
|
Comment: '/private/comment',
|
||||||
CommentShared: '/shared/comment',
|
CommentShared: '/shared/comment',
|
||||||
|
|
@ -26,96 +39,47 @@ export const FolderMetadataKeys = {
|
||||||
// Kolab
|
// Kolab
|
||||||
KolabFolderType: '/private/vendor/kolab/folder-type',
|
KolabFolderType: '/private/vendor/kolab/folder-type',
|
||||||
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const FolderSortMode = {
|
ComposeType = {
|
||||||
DateDesc: '', // default 'REVERSE DATE'
|
Empty: 0,
|
||||||
DateAsc: 'DATE',
|
Reply: 1,
|
||||||
FromDesc: 'REVERSE FROM',
|
ReplyAll: 2,
|
||||||
FromAsc: 'FROM',
|
Forward: 3,
|
||||||
SizeDesc: 'REVERSE SIZE',
|
ForwardAsAttachment: 4,
|
||||||
SizeAsc: 'SIZE',
|
Draft: 5,
|
||||||
SubjectDesc: 'REVERSE SUBJECT',
|
EditAsNew: 6
|
||||||
SubjectAsc: 'SUBJECT'
|
},
|
||||||
// ToDesc: 'REVERSE TO',
|
|
||||||
// ToAsc: 'TO',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
export const ComposeType = {
|
|
||||||
Empty: 'empty',
|
|
||||||
Reply: 'reply',
|
|
||||||
ReplyAll: 'replyall',
|
|
||||||
Forward: 'forward',
|
|
||||||
ForwardAsAttachment: 'forward-as-attachment',
|
|
||||||
Draft: 'draft',
|
|
||||||
EditAsNew: 'editasnew'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const SetSystemFoldersNotification = {
|
ClientSideKeyNameExpandedFolders = 3,
|
||||||
None: 0,
|
ClientSideKeyNameFolderListSize = 4,
|
||||||
Sent: 1,
|
ClientSideKeyNameMessageListSize = 5,
|
||||||
Draft: 2,
|
ClientSideKeyNameLastSignMe = 7,
|
||||||
Spam: 3,
|
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
||||||
Trash: 4,
|
ClientSideKeyNameMessageAttachmentControls = 10,
|
||||||
Archive: 5
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const ClientSideKeyName = {
|
MessageSetAction = {
|
||||||
ExpandedFolders: 3,
|
|
||||||
FolderListSize: 4,
|
|
||||||
MessageListSize: 5,
|
|
||||||
LastReplyAction: 6,
|
|
||||||
LastSignMe: 7,
|
|
||||||
MessageHeaderFullInfo: 9,
|
|
||||||
MessageAttachmentControls: 10
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
export const MessageSetAction = {
|
|
||||||
SetSeen: 0,
|
SetSeen: 0,
|
||||||
UnsetSeen: 1,
|
UnsetSeen: 1,
|
||||||
SetFlag: 2,
|
SetFlag: 2,
|
||||||
UnsetFlag: 3
|
UnsetFlag: 3,
|
||||||
};
|
SetDeleted: 4,
|
||||||
|
UnsetDeleted: 5
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const MessagePriority = {
|
//LayoutNoView = 0,
|
||||||
Low: 5,
|
LayoutSideView = 1,
|
||||||
Normal: 3,
|
LayoutBottomView = 2
|
||||||
High: 1
|
;
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
export const EditorDefaultType = {
|
|
||||||
Html: 'Html',
|
|
||||||
Plain: 'Plain',
|
|
||||||
HtmlForced: 'HtmlForced',
|
|
||||||
PlainForced: 'PlainForced'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
export const Layout = {
|
|
||||||
NoPreview: 0,
|
|
||||||
SidePreview: 1,
|
|
||||||
BottomPreview: 2
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
/* eslint key-spacing: 0 */
|
/* eslint key-spacing: 0 */
|
||||||
/* eslint quote-props: 0 */
|
/* eslint quote-props: 0 */
|
||||||
|
|
||||||
import { arrayLength } from 'Common/Utils';
|
import { arrayLength, pInt } from 'Common/Utils';
|
||||||
|
|
||||||
|
export const RFC822 = 'message/rfc822';
|
||||||
|
|
||||||
const
|
const
|
||||||
cache = {},
|
cache = {},
|
||||||
|
|
@ -12,8 +14,8 @@ const
|
||||||
lowerCase = text => text.toLowerCase().trim(),
|
lowerCase = text => text.toLowerCase().trim(),
|
||||||
|
|
||||||
exts = {
|
exts = {
|
||||||
eml: 'message/rfc822',
|
eml: RFC822,
|
||||||
mime: 'message/rfc822',
|
mime: RFC822,
|
||||||
vcard: 'text/vcard',
|
vcard: 'text/vcard',
|
||||||
vcf: 'text/vcard',
|
vcf: 'text/vcard',
|
||||||
htm: 'text/html',
|
htm: 'text/html',
|
||||||
|
|
@ -22,11 +24,14 @@ const
|
||||||
ics: 'text/calendar',
|
ics: 'text/calendar',
|
||||||
xml: 'text/xml',
|
xml: 'text/xml',
|
||||||
json: app+'json',
|
json: app+'json',
|
||||||
asc: app+'pgp-signature',
|
// asc: app+'pgp-signature',
|
||||||
|
// asc: app+'pgp-keys',
|
||||||
p10: app+'pkcs10',
|
p10: app+'pkcs10',
|
||||||
p7c: app+'pkcs7-mime',
|
p7c: app+'pkcs7-mime',
|
||||||
p7m: app+'pkcs7-mime',
|
p7m: app+'pkcs7-mime',
|
||||||
p7s: app+'pkcs7-signature',
|
p7s: app+'pkcs7-signature',
|
||||||
|
p12: app+'pkcs12',
|
||||||
|
pfx: app+'x-pkcs12',
|
||||||
torrent: app+'x-bittorrent',
|
torrent: app+'x-bittorrent',
|
||||||
|
|
||||||
// scripts
|
// scripts
|
||||||
|
|
@ -115,7 +120,8 @@ export const FileType = {
|
||||||
Spreadsheet: 'spreadsheet',
|
Spreadsheet: 'spreadsheet',
|
||||||
Presentation: 'presentation',
|
Presentation: 'presentation',
|
||||||
Certificate: 'certificate',
|
Certificate: 'certificate',
|
||||||
Archive: 'archive'
|
Archive: 'archive',
|
||||||
|
Calendar: 'calendar'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FileInfo = {
|
export const FileInfo = {
|
||||||
|
|
@ -132,7 +138,7 @@ export const FileInfo = {
|
||||||
getContentType: fileName => {
|
getContentType: fileName => {
|
||||||
fileName = lowerCase(fileName);
|
fileName = lowerCase(fileName);
|
||||||
if ('winmail.dat' === fileName) {
|
if ('winmail.dat' === fileName) {
|
||||||
return app + 'ms-tnef';
|
return app + 'vnd.ms-tnef';
|
||||||
}
|
}
|
||||||
let ext = fileName.split('.').pop();
|
let ext = fileName.split('.').pop();
|
||||||
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
|
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
|
||||||
|
|
@ -160,7 +166,7 @@ export const FileInfo = {
|
||||||
*/
|
*/
|
||||||
getType: (ext, mimeType) => {
|
getType: (ext, mimeType) => {
|
||||||
ext = lowerCase(ext);
|
ext = lowerCase(ext);
|
||||||
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
|
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv').replace('x-','');
|
||||||
|
|
||||||
let key = ext + mimeType;
|
let key = ext + mimeType;
|
||||||
if (cache[key]) {
|
if (cache[key]) {
|
||||||
|
|
@ -169,7 +175,7 @@ export const FileInfo = {
|
||||||
|
|
||||||
let result = FileType.Unknown;
|
let result = FileType.Unknown;
|
||||||
const mimeTypeParts = mimeType.split('/'),
|
const mimeTypeParts = mimeType.split('/'),
|
||||||
type = mimeTypeParts[1].replace('x-','').replace('-compressed',''),
|
type = mimeTypeParts[1].replace('-compressed',''),
|
||||||
match = str => mimeType.includes(str),
|
match = str => mimeType.includes(str),
|
||||||
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
|
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
|
||||||
|
|
||||||
|
|
@ -186,9 +192,12 @@ export const FileInfo = {
|
||||||
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
|
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
|
||||||
result = FileType.Code;
|
result = FileType.Code;
|
||||||
break;
|
break;
|
||||||
case 'eml' == ext || ['message/delivery-status', 'message/rfc822'].includes(mimeType):
|
case 'eml' == ext || ['message/delivery-status', RFC822].includes(mimeType):
|
||||||
result = FileType.Eml;
|
result = FileType.Eml;
|
||||||
break;
|
break;
|
||||||
|
case 'ics' == ext || mimeType == 'text/calendar':
|
||||||
|
result = FileType.Calendar;
|
||||||
|
break;
|
||||||
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
|
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
|
||||||
result = FileType.Text;
|
result = FileType.Text;
|
||||||
break;
|
break;
|
||||||
|
|
@ -198,9 +207,8 @@ export const FileInfo = {
|
||||||
case 'pdf' == type || 'pdf' == ext:
|
case 'pdf' == type || 'pdf' == ext:
|
||||||
result = FileType.Pdf;
|
result = FileType.Pdf;
|
||||||
break;
|
break;
|
||||||
case [app+'pgp-signature', app+'pgp-keys'].includes(mimeType)
|
case [app+'pgp-signature', app+'pgp-keys', exts.p7m, exts.p7s, exts.p12, exts.pfx].includes(mimeType)
|
||||||
|| ['asc', 'pem', 'ppk'].includes(ext)
|
|| ['asc', 'pem', 'ppk', 'p7s', 'p7m', 'p12', 'pfx'].includes(ext):
|
||||||
|| [app+'pkcs7-signature'].includes(mimeType) || 'p7s' == ext:
|
|
||||||
result = FileType.Certificate;
|
result = FileType.Certificate;
|
||||||
break;
|
break;
|
||||||
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|
||||||
|
|
@ -239,6 +247,7 @@ export const FileInfo = {
|
||||||
case FileType.Certificate:
|
case FileType.Certificate:
|
||||||
case FileType.Spreadsheet:
|
case FileType.Spreadsheet:
|
||||||
case FileType.Presentation:
|
case FileType.Presentation:
|
||||||
|
case FileType.Calendar:
|
||||||
return result + '-' + fileType;
|
return result + '-' + fileType;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -256,7 +265,7 @@ export const FileInfo = {
|
||||||
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
|
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
|
||||||
.validUnique();
|
.validUnique();
|
||||||
|
|
||||||
return (icons && 1 === icons.length && 'icon-file' !== icons[0])
|
return (1 === icons?.length && 'icon-file' !== icons[0])
|
||||||
? icons[0]
|
? icons[0]
|
||||||
: 'icon-attachment';
|
: 'icon-attachment';
|
||||||
}
|
}
|
||||||
|
|
@ -265,8 +274,8 @@ export const FileInfo = {
|
||||||
},
|
},
|
||||||
|
|
||||||
friendlySize: bytes => {
|
friendlySize: bytes => {
|
||||||
bytes = parseInt(bytes, 10) || 0;
|
bytes = pInt(bytes);
|
||||||
let i = Math.floor(Math.log(bytes) / Math.log(1024));
|
let i = bytes ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0;
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
|
return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
211
dev/Common/Folders.js
Normal file
211
dev/Common/Folders.js
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { RFC822 } from 'Common/File';
|
||||||
|
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
|
||||||
|
import { baseCollator } from 'Common/Translator';
|
||||||
|
import { SettingsGet } from 'Common/Globals';
|
||||||
|
import { isArray, arrayLength, pInt } from 'Common/Utils';
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
let refreshInterval,
|
||||||
|
// Default every 15 minutes
|
||||||
|
refreshFoldersInterval = 900000;
|
||||||
|
|
||||||
|
export const
|
||||||
|
|
||||||
|
setRefreshFoldersInterval = minutes => {
|
||||||
|
refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
refreshInterval = setInterval(() => {
|
||||||
|
const cF = FolderUserStore.currentFolderFullName(),
|
||||||
|
iF = getFolderInboxName();
|
||||||
|
folderInformation(iF);
|
||||||
|
iF === cF || folderInformation(cF);
|
||||||
|
folderInformationMultiply();
|
||||||
|
}, refreshFoldersInterval);
|
||||||
|
},
|
||||||
|
|
||||||
|
sortFolders = folders => {
|
||||||
|
try {
|
||||||
|
let collator = baseCollator(true);
|
||||||
|
folders.sort((a, b) =>
|
||||||
|
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array=} aDisabled
|
||||||
|
* @param {Array=} aHeaderLines
|
||||||
|
* @param {Function=} fRenameCallback
|
||||||
|
* @param {Function=} fDisableCallback
|
||||||
|
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
folderListOptionsBuilder = (
|
||||||
|
aDisabled,
|
||||||
|
aHeaderLines,
|
||||||
|
fRenameCallback,
|
||||||
|
fDisableCallback
|
||||||
|
) => {
|
||||||
|
const
|
||||||
|
aResult = [],
|
||||||
|
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||||
|
// FolderSystemPopupView should always be true
|
||||||
|
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
||||||
|
isDisabled = fDisableCallback || (item => !item.selectable() || aDisabled.includes(item.fullName)),
|
||||||
|
|
||||||
|
foldersWalk = folders => {
|
||||||
|
folders.forEach(oItem => {
|
||||||
|
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||||
|
aResult.push({
|
||||||
|
id: oItem.fullName,
|
||||||
|
name:
|
||||||
|
sDeepPrefix.repeat(oItem.deep) +
|
||||||
|
fRenameCallback(oItem),
|
||||||
|
system: false,
|
||||||
|
disabled: isDisabled(oItem)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
foldersWalk(oItem.subFolders());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
fDisableCallback = fDisableCallback || (() => false);
|
||||||
|
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
||||||
|
isArray(aDisabled) || (aDisabled = []);
|
||||||
|
|
||||||
|
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
||||||
|
aResult.push({
|
||||||
|
id: line[0],
|
||||||
|
name: line[1],
|
||||||
|
system: false,
|
||||||
|
disabled: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
foldersWalk(FolderUserStore.folderList());
|
||||||
|
|
||||||
|
return aResult;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folder
|
||||||
|
* @param {Array=} list = []
|
||||||
|
*/
|
||||||
|
folderInformation = (folder, list) => {
|
||||||
|
if (folder?.trim()) {
|
||||||
|
let count = 1;
|
||||||
|
const uids = [];
|
||||||
|
|
||||||
|
if (arrayLength(list)) {
|
||||||
|
list.forEach(messageListItem => {
|
||||||
|
uids.push(messageListItem.uid);
|
||||||
|
messageListItem.threads.forEach(uid => uids.push(uid));
|
||||||
|
});
|
||||||
|
count = uids.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
Remote.request('FolderInformation', (iError, data) => {
|
||||||
|
if (!iError && data.Result) {
|
||||||
|
const result = data.Result,
|
||||||
|
folderFromCache = getFolderFromCacheList(result.name);
|
||||||
|
if (folderFromCache) {
|
||||||
|
const oldHash = folderFromCache.etag,
|
||||||
|
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
|
||||||
|
|
||||||
|
// folderFromCache.revivePropertiesFromJson(result);
|
||||||
|
folderFromCache.expires = Date.now();
|
||||||
|
folderFromCache.uidNext = result.uidNext;
|
||||||
|
folderFromCache.etag = result.etag;
|
||||||
|
folderFromCache.totalEmails(result.totalEmails);
|
||||||
|
folderFromCache.unreadEmails(result.unreadEmails);
|
||||||
|
|
||||||
|
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.newMessages);
|
||||||
|
|
||||||
|
if (!oldHash || unreadCountChange || result.etag !== oldHash) {
|
||||||
|
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
||||||
|
MessagelistUserStore.reload();
|
||||||
|
/*
|
||||||
|
} else if (getFolderInboxName() === folderFromCache.fullName) {
|
||||||
|
// Remote.messageList(null, {folder: getFolderFromCacheList(getFolderInboxName())}, true);
|
||||||
|
Remote.messageList(null, {folder: getFolderInboxName()}, true);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
folder: folder,
|
||||||
|
flagsUids: uids,
|
||||||
|
uidNext: getFolderFromCacheList(folder)?.uidNext || 0 // Used to check for new messages
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean=} boot = false
|
||||||
|
*/
|
||||||
|
folderInformationMultiply = (boot = false) => {
|
||||||
|
const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
|
||||||
|
if (arrayLength(folders)) {
|
||||||
|
Remote.request('FolderInformationMultiply', (iError, oData) => {
|
||||||
|
if (!iError && arrayLength(oData.Result)) {
|
||||||
|
const utc = Date.now();
|
||||||
|
oData.Result.forEach(item => {
|
||||||
|
const folder = getFolderFromCacheList(item.name);
|
||||||
|
if (folder) {
|
||||||
|
const oldHash = folder.etag,
|
||||||
|
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
|
||||||
|
|
||||||
|
// folder.revivePropertiesFromJson(item);
|
||||||
|
folder.expires = utc;
|
||||||
|
folder.etag = item.etag;
|
||||||
|
folder.totalEmails(item.totalEmails);
|
||||||
|
folder.unreadEmails(item.unreadEmails);
|
||||||
|
|
||||||
|
if (!oldHash || item.etag !== oldHash) {
|
||||||
|
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||||
|
MessagelistUserStore.reload();
|
||||||
|
}
|
||||||
|
} else if (unreadCountChange
|
||||||
|
&& folder.fullName === FolderUserStore.currentFolderFullName()
|
||||||
|
&& MessagelistUserStore.length) {
|
||||||
|
folderInformation(folder.fullName, MessagelistUserStore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
boot && setTimeout(() => folderInformationMultiply(true), 2000);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
folders: folders
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
dropFilesInFolder = (sFolderFullName, files) => {
|
||||||
|
let count = files.length;
|
||||||
|
for (const file of files) {
|
||||||
|
if (RFC822 === file.type) {
|
||||||
|
let data = new FormData;
|
||||||
|
data.append('folder', sFolderFullName);
|
||||||
|
data.append('appendFile', file);
|
||||||
|
Remote.request('FolderAppend', (iError, data)=>{
|
||||||
|
iError && console.error(data.message);
|
||||||
|
0 == --count
|
||||||
|
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
||||||
|
&& MessagelistUserStore.reload(true, true);
|
||||||
|
}, data);
|
||||||
|
} else {
|
||||||
|
--count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
22
dev/Common/Fullscreen.js
Normal file
22
dev/Common/Fullscreen.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { doc, appEl, $htmlCL } from 'Common/Globals';
|
||||||
|
|
||||||
|
// Fullscreen must be on app, else other popups fail
|
||||||
|
export const
|
||||||
|
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
|
||||||
|
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
||||||
|
isFullscreen = ko.observable(false),
|
||||||
|
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
|
||||||
|
|
||||||
|
if (appEl) {
|
||||||
|
let event = 'fullscreenchange';
|
||||||
|
if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
|
||||||
|
appEl.requestFullscreen = appEl.webkitRequestFullscreen;
|
||||||
|
event = 'webkit'+event;
|
||||||
|
}
|
||||||
|
if (appEl.requestFullscreen) {
|
||||||
|
doc.addEventListener(event, () => {
|
||||||
|
isFullscreen(appFullscreen());
|
||||||
|
$htmlCL.toggle('rl-fullscreen', appFullscreen());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { Scope } from 'Common/Enums';
|
|
||||||
|
|
||||||
let keyScopeFake = Scope.All;
|
let keyScopeFake = 'all';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
ScopeMenu = 'Menu',
|
||||||
|
|
||||||
doc = document,
|
doc = document,
|
||||||
|
|
||||||
|
|
@ -11,17 +11,18 @@ export const
|
||||||
|
|
||||||
elementById = id => doc.getElementById(id),
|
elementById = id => doc.getElementById(id),
|
||||||
|
|
||||||
exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
appEl = elementById('rl-app'),
|
||||||
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
|
|
||||||
|
|
||||||
Settings = rl.settings,
|
Settings = rl.settings,
|
||||||
SettingsGet = Settings.get,
|
SettingsGet = Settings.get,
|
||||||
SettingsCapa = Settings.capa,
|
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
|
||||||
|
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
||||||
|
|
||||||
|
dropdowns = [],
|
||||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
||||||
|
|
||||||
moveAction = ko.observable(false),
|
|
||||||
leftPanelDisabled = ko.observable(false),
|
leftPanelDisabled = ko.observable(false),
|
||||||
|
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
|
||||||
|
|
||||||
createElement = (name, attr) => {
|
createElement = (name, attr) => {
|
||||||
let el = doc.createElement(name);
|
let el = doc.createElement(name);
|
||||||
|
|
@ -29,37 +30,50 @@ export const
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})),
|
fireEvent = (name, detail, cancelable) => dispatchEvent(
|
||||||
|
new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
|
||||||
|
),
|
||||||
|
|
||||||
// keys
|
stopEvent = event => {
|
||||||
keyScopeReal = ko.observable(Scope.All),
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
|
formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
|
||||||
|
|
||||||
|
addShortcut = (...args) => shortcuts.add(...args),
|
||||||
|
|
||||||
|
registerShortcut = (keys, modifiers, scopes, method) =>
|
||||||
|
addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
|
||||||
|
|
||||||
|
addEventsListener = (element, events, fn, options) =>
|
||||||
|
events.forEach(event => element.addEventListener(event, fn, options)),
|
||||||
|
|
||||||
|
addEventsListeners = (element, events) =>
|
||||||
|
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
|
||||||
|
|
||||||
|
// keys / shortcuts
|
||||||
|
keyScopeReal = ko.observable('all'),
|
||||||
keyScope = value => {
|
keyScope = value => {
|
||||||
if (value) {
|
if (!value) {
|
||||||
if (Scope.Menu !== value) {
|
|
||||||
keyScopeFake = value;
|
|
||||||
if (dropdownVisibility()) {
|
|
||||||
value = Scope.Menu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keyScopeReal(value);
|
|
||||||
shortcuts.setScope(value);
|
|
||||||
} else {
|
|
||||||
return keyScopeFake;
|
return keyScopeFake;
|
||||||
}
|
}
|
||||||
|
if (ScopeMenu !== value) {
|
||||||
|
keyScopeFake = value;
|
||||||
|
if (dropdownVisibility()) {
|
||||||
|
value = ScopeMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyScopeReal(value);
|
||||||
|
shortcuts.setScope(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
dropdownVisibility.subscribe(value => {
|
dropdownVisibility.subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
keyScope(Scope.Menu);
|
keyScope(ScopeMenu);
|
||||||
} else if (Scope.Menu === shortcuts.getScope()) {
|
} else if (ScopeMenu === shortcuts.getScope()) {
|
||||||
keyScope(keyScopeFake);
|
keyScope(keyScopeFake);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
|
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value));
|
||||||
leftPanelDisabled.subscribe(value => {
|
|
||||||
value && moveAction() && moveAction(false);
|
|
||||||
$htmlCL.toggle('rl-left-panel-disabled', value);
|
|
||||||
});
|
|
||||||
|
|
||||||
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
161
dev/Common/HtmlEditor.js
Normal file
161
dev/Common/HtmlEditor.js
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
|
export const
|
||||||
|
WYSIWYGS = ko.observableArray();
|
||||||
|
|
||||||
|
WYSIWYGS.push({
|
||||||
|
name: 'Squire',
|
||||||
|
construct: (owner, container, onReady) => onReady(new SquireUI(container))
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push({name, construct});
|
||||||
|
|
||||||
|
export class HtmlEditor {
|
||||||
|
/**
|
||||||
|
* @param {Object} element
|
||||||
|
* @param {Function=} onBlur
|
||||||
|
* @param {Function=} onReady
|
||||||
|
* @param {Function=} onModeChange
|
||||||
|
*/
|
||||||
|
constructor(element, onReady = null, onModeChange = null, onBlur = null) {
|
||||||
|
this.blurTimer = 0;
|
||||||
|
|
||||||
|
this.onBlur = onBlur;
|
||||||
|
this.onModeChange = onModeChange;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
onReady = onReady ? [onReady] : [];
|
||||||
|
this.onReady = fn => onReady.push(fn);
|
||||||
|
const which = SettingsUserStore.editorWysiwyg(),
|
||||||
|
wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name);
|
||||||
|
wysiwyg.construct(this, element, editor => setTimeout(()=>{
|
||||||
|
this.editor = editor;
|
||||||
|
editor.on('blur', () => this.blurTrigger());
|
||||||
|
editor.on('focus', () => clearTimeout(this.blurTimer));
|
||||||
|
editor.on('mode', () => {
|
||||||
|
this.blurTrigger();
|
||||||
|
this.onModeChange?.(!this.isPlain());
|
||||||
|
});
|
||||||
|
this.onReady = fn => fn();
|
||||||
|
onReady.forEach(fn => fn());
|
||||||
|
},1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blurTrigger() {
|
||||||
|
if (this.onBlur) {
|
||||||
|
clearTimeout(this.blurTimer);
|
||||||
|
this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isHtml() {
|
||||||
|
return this.editor ? !this.isPlain() : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isPlain() {
|
||||||
|
return this.editor ? 'plain' === this.editor.mode : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
clearCachedSignature() {
|
||||||
|
this.onReady(() => this.editor.execCommand('insertSignature', {
|
||||||
|
clearCache: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} signature
|
||||||
|
* @param {bool} html
|
||||||
|
* @param {bool} insertBefore
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
setSignature(signature, html, insertBefore = false) {
|
||||||
|
this.onReady(() => this.editor.execCommand('insertSignature', {
|
||||||
|
isHtml: html,
|
||||||
|
insertBefore: insertBefore,
|
||||||
|
signature: signature
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean=} wrapIsHtml = false
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getData() {
|
||||||
|
let result = '';
|
||||||
|
if (this.editor) {
|
||||||
|
try {
|
||||||
|
if (this.isPlain()) {
|
||||||
|
result = this.editor.getPlainData();
|
||||||
|
} else {
|
||||||
|
result = this.editor.getData();
|
||||||
|
}
|
||||||
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getDataWithHtmlMark() {
|
||||||
|
return (this.isHtml() ? ':HTML:' : '') + this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
modeWysiwyg() {
|
||||||
|
this.onReady(() => this.editor.setMode('wysiwyg'));
|
||||||
|
}
|
||||||
|
modePlain() {
|
||||||
|
this.onReady(() => this.editor.setMode('plain'));
|
||||||
|
}
|
||||||
|
|
||||||
|
setHtmlOrPlain(text) {
|
||||||
|
text.startsWith(':HTML:')
|
||||||
|
? this.setHtml(text.slice(6))
|
||||||
|
: this.setPlain(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(mode, data) {
|
||||||
|
this.onReady(() => {
|
||||||
|
const editor = this.editor;
|
||||||
|
this.clearCachedSignature();
|
||||||
|
try {
|
||||||
|
editor.setMode(mode);
|
||||||
|
if (this.isPlain()) {
|
||||||
|
editor.setPlainData(data);
|
||||||
|
} else {
|
||||||
|
editor.setData(data);
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setHtml(html) {
|
||||||
|
this.setData('wysiwyg', html/*.replace(/<p[^>]*><\/p>/gi, '')*/);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlain(txt) {
|
||||||
|
this.setData('plain', txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.onReady(() => this.editor.focus());
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
this.onReady(() => this.editor.blur());
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
import { pString, pInt } from 'Common/Utils';
|
import { pInt } from 'Common/Utils';
|
||||||
import { Settings } from 'Common/Globals';
|
import { doc, Settings, SettingsAdmin } from 'Common/Globals';
|
||||||
|
|
||||||
const
|
const
|
||||||
ROOT = './',
|
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
||||||
HASH_PREFIX = '#/',
|
HASH_PREFIX = '#/',
|
||||||
SERVER_PREFIX = './?',
|
|
||||||
VERSION = Settings.app('version'),
|
|
||||||
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
|
|
||||||
|
|
||||||
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
|
adminPath = () => rl.adminArea() && !SettingsAdmin('host'),
|
||||||
|
|
||||||
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
|
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
|
||||||
|
|
||||||
export const
|
export const
|
||||||
SUB_QUERY_PREFIX = '&q[]=',
|
SUB_QUERY_PREFIX = '&q[]=',
|
||||||
|
|
@ -19,12 +16,12 @@ export const
|
||||||
* @param {string=} startupUrl
|
* @param {string=} startupUrl
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
root = (startupUrl = '') => HASH_PREFIX + pString(startupUrl),
|
root = () => HASH_PREFIX,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
logoutLink = () => adminPath() ? prefix() : ROOT,
|
logoutLink = () => adminPath() ? prefix() : BASE,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
|
|
@ -33,29 +30,24 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
serverRequestRaw = (type, hash) =>
|
serverRequestRaw = (type, hash) =>
|
||||||
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
|
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||||
+ '0/' // AuthAccountHash ?
|
+ '0/' // Settings.get('accountHash') ?
|
||||||
+ (type
|
+ (type
|
||||||
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
||||||
: ''),
|
: ''),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} download
|
* @param {string} download
|
||||||
* @param {string=} customSpecSuffix
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
attachmentDownload = (download, customSpecSuffix) =>
|
attachmentDownload = (download) =>
|
||||||
serverRequestRaw('Download', download, customSpecSuffix),
|
serverRequestRaw('Download', download),
|
||||||
|
|
||||||
proxy = url =>
|
proxy = url =>
|
||||||
SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
BASE + '?/ProxyExternal/'
|
||||||
/*
|
// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
|
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
'Rnd' => \md5(\microtime(true)),
|
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
|
||||||
'Token' => Utils::GetConnectionToken(),
|
|
||||||
'Url' => $sUrl
|
|
||||||
)).'/';
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
|
|
@ -63,32 +55,40 @@ export const
|
||||||
*/
|
*/
|
||||||
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
||||||
|
|
||||||
|
// Is '?/Css/0/Admin' needed?
|
||||||
|
cssLink = theme => BASE + '?/Css/0/User/-/' + encodeURI(theme) + '/-/' + Date.now() + '/Hash/-/Json/',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} lang
|
* @param {string} lang
|
||||||
* @param {boolean} isAdmin
|
* @param {boolean} isAdmin
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
langLink = (lang, isAdmin) =>
|
langLink = (lang, isAdmin) =>
|
||||||
SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/',
|
BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App')
|
||||||
|
+ '/' + encodeURI(lang)
|
||||||
|
+ '/' + Settings.app('version') + '/',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
staticLink = path => VERSION_PREFIX + 'static/' + path,
|
staticLink = path => Settings.app('webVersionPath') + 'static/' + path,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} theme
|
* @param {string} theme
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
themePreviewLink = theme => {
|
themePreviewLink = theme => {
|
||||||
let prefix = VERSION_PREFIX;
|
if (theme.endsWith('@nextcloud')) {
|
||||||
if ('@custom' === theme.slice(-7)) {
|
theme = theme.slice(0, theme.length - 10).trim();
|
||||||
theme = theme.slice(0, theme.length - 7).trim();
|
return parent.OC.webroot + '/themes/' + encodeURI(theme) + '/snappymail/preview.png';
|
||||||
prefix = Settings.app('webPath') || '';
|
|
||||||
}
|
}
|
||||||
|
let path = 'webVersionPath';
|
||||||
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
|
if (theme.endsWith('@custom')) {
|
||||||
|
theme = theme.slice(0, theme.length - 7).trim();
|
||||||
|
path = 'webPath';
|
||||||
|
}
|
||||||
|
return Settings.app(path) + 'themes/' + encodeURI(theme) + '/images/preview.png';
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -120,22 +120,24 @@ export const
|
||||||
* @param {number=} threadUid = 0
|
* @param {number=} threadUid = 0
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
mailBox = (folder, page, search, threadUid) => {
|
mailBox = (folder, page, search, threadUid, messageUid) => {
|
||||||
let result = [HASH_PREFIX + 'mailbox'];
|
let result = [HASH_PREFIX + 'mailbox'];
|
||||||
|
|
||||||
if (folder) {
|
if (folder) {
|
||||||
result.push(folder + (threadUid ? '~' + threadUid : ''));
|
result.push(folder + (threadUid ? '~' + threadUid : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
page = pInt(page, 1);
|
if (messageUid) {
|
||||||
if (1 < page) {
|
result.push('m' + messageUid);
|
||||||
result.push('p' + page);
|
} else {
|
||||||
}
|
page = pInt(page, 1);
|
||||||
|
if (1 < page) {
|
||||||
search = pString(search);
|
result.push('p' + page);
|
||||||
if (search) {
|
}
|
||||||
result.push(encodeURI(search));
|
search && result.push(encodeURI(search));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.join('/');
|
return result.join('/');
|
||||||
};
|
},
|
||||||
|
|
||||||
|
mailBoxMessage = (folder, messageUid) => mailBox(folder, 1, '', 0, pInt(messageUid));
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import { i18n } from 'Common/Translator';
|
|
||||||
|
|
||||||
export function timestampToString(timeStampInUTC, formatStr) {
|
|
||||||
const now = Date.now(),
|
|
||||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
|
||||||
|
|
||||||
if (31536000000 < time) {
|
|
||||||
const m = new Date(time);
|
|
||||||
switch (formatStr) {
|
|
||||||
case 'FROMNOW':
|
|
||||||
return m.fromNow();
|
|
||||||
case 'SHORT': {
|
|
||||||
if (4 >= (now - time) / 3600000)
|
|
||||||
return m.fromNow();
|
|
||||||
const mt = m.getTime(), date = new Date,
|
|
||||||
dt = date.setHours(0,0,0,0);
|
|
||||||
if (mt > dt)
|
|
||||||
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
|
||||||
if (mt > dt - 86400000)
|
|
||||||
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
|
||||||
if (date.getFullYear() === m.getFullYear())
|
|
||||||
return m.format('d M');
|
|
||||||
return m.format('LL');
|
|
||||||
}
|
|
||||||
case 'FULL':
|
|
||||||
return m.format('LLL');
|
|
||||||
default:
|
|
||||||
return m.format(formatStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function timeToNode(element, time) {
|
|
||||||
try {
|
|
||||||
if (time) {
|
|
||||||
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
|
||||||
} else {
|
|
||||||
time = Date.parse(element.dateTime) / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = element.dataset.momentFormat;
|
|
||||||
if (key) {
|
|
||||||
element.textContent = timestampToString(time, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((key = element.dataset.momentFormatTitle)) {
|
|
||||||
element.title = timestampToString(time, key);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// prevent knockout crashes
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,7 +12,7 @@ const USER_VIEW_MODELS_HOOKS = [],
|
||||||
* @param {?number=} timeout
|
* @param {?number=} timeout
|
||||||
*/
|
*/
|
||||||
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
|
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
|
||||||
rl.app && rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
|
rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -39,9 +39,9 @@ rl.addSettingsViewModelForAdmin = (SettingsViewModelClass, template, labelName,
|
||||||
* @param {boolean} admin
|
* @param {boolean} admin
|
||||||
*/
|
*/
|
||||||
export function runSettingsViewModelHooks(admin) {
|
export function runSettingsViewModelHooks(admin) {
|
||||||
(admin ? ADMIN_VIEW_MODELS_HOOKS : USER_VIEW_MODELS_HOOKS).forEach(view => {
|
(admin ? ADMIN_VIEW_MODELS_HOOKS : USER_VIEW_MODELS_HOOKS).forEach(view =>
|
||||||
settingsAddViewModel(view[0], view[1], view[2], view[3]);
|
settingsAddViewModel(...view)
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,10 +49,7 @@ export function runSettingsViewModelHooks(admin) {
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @returns {?}
|
* @returns {?}
|
||||||
*/
|
*/
|
||||||
rl.pluginSettingsGet = (pluginSection, name) => {
|
rl.pluginSettingsGet = (pluginSection, name) =>
|
||||||
let plugins = SettingsGet('Plugins');
|
SettingsGet('Plugins')?.[pluginSection]?.[name];
|
||||||
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
|
|
||||||
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
rl.pluginPopupView = AbstractViewPopup;
|
rl.pluginPopupView = AbstractViewPopup;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
import { koComputable } from 'External/ko';
|
import { koComputable } from 'External/ko';
|
||||||
|
|
||||||
|
|
@ -6,11 +7,13 @@ import { koComputable } from 'External/ko';
|
||||||
oCallbacks:
|
oCallbacks:
|
||||||
ItemSelect
|
ItemSelect
|
||||||
MiddleClick
|
MiddleClick
|
||||||
AutoSelect
|
canSelect
|
||||||
ItemGetUid
|
ItemGetUid
|
||||||
UpOrDown
|
UpOrDown
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
let shiftStart;
|
||||||
|
|
||||||
export class Selector {
|
export class Selector {
|
||||||
/**
|
/**
|
||||||
* @param {koProperty} koList
|
* @param {koProperty} koList
|
||||||
|
|
@ -18,89 +21,86 @@ export class Selector {
|
||||||
* @param {koProperty} koFocusedItem
|
* @param {koProperty} koFocusedItem
|
||||||
* @param {string} sItemSelector
|
* @param {string} sItemSelector
|
||||||
* @param {string} sItemCheckedSelector
|
* @param {string} sItemCheckedSelector
|
||||||
* @param {string} sItemFocusedSelector
|
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
koList,
|
koList,
|
||||||
koSelectedItem,
|
koSelectedItem,
|
||||||
koFocusedItem,
|
koFocusedItem,
|
||||||
sItemSelector,
|
sItemSelector,
|
||||||
sItemCheckedSelector,
|
sItemCheckedSelector
|
||||||
sItemFocusedSelector
|
|
||||||
) {
|
) {
|
||||||
|
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
|
||||||
|
koSelectedItem = (koSelectedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [null, 'selected'] });
|
||||||
|
|
||||||
this.list = koList;
|
this.list = koList;
|
||||||
this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
|
this.listChecked = koComputable(() => koList.filter(item => item.checked())).extend({ rateLimit: 0 });
|
||||||
this.isListChecked = koComputable(() => 0 < this.listChecked().length);
|
|
||||||
|
|
||||||
this.focusedItem = koFocusedItem || ko.observable(null);
|
this.focusedItem = koFocusedItem;
|
||||||
this.selectedItem = koSelectedItem || ko.observable(null);
|
this.selectedItem = koSelectedItem;
|
||||||
|
|
||||||
this.selectedItemUseCallback = true;
|
|
||||||
|
|
||||||
this.iSelectNextHelper = 0;
|
this.iSelectNextHelper = 0;
|
||||||
this.iFocusedNextHelper = 0;
|
this.iFocusedNextHelper = 0;
|
||||||
this.oContentScrollable;
|
// this.oContentScrollable = null;
|
||||||
|
|
||||||
this.sItemSelector = sItemSelector;
|
this.sItemSelector = sItemSelector;
|
||||||
this.sItemCheckedSelector = sItemCheckedSelector;
|
this.sItemCheckedSelector = sItemCheckedSelector;
|
||||||
this.sItemFocusedSelector = sItemFocusedSelector;
|
this.sItemFocusedSelector = sItemSelector + '.focused';
|
||||||
|
|
||||||
this.sLastUid = '';
|
this.sLastUid = '';
|
||||||
this.oCallbacks = {};
|
this.oCallbacks = {};
|
||||||
|
|
||||||
const itemSelectedThrottle = (item => this.itemSelected(item)).debounce(300);
|
const
|
||||||
|
itemSelected = item => {
|
||||||
|
if (koList.hasChecked()) {
|
||||||
|
item || this.oCallbacks.ItemSelect?.(null);
|
||||||
|
} else if (item) {
|
||||||
|
this.oCallbacks.ItemSelect?.(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
this.listChecked.subscribe((items) => {
|
itemSelectedThrottle = (item => itemSelected(item)).debounce(300);
|
||||||
|
|
||||||
|
this.listChecked.subscribe(items => {
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
if (null === this.selectedItem()) {
|
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
|
||||||
if (this.selectedItem.valueHasMutated) {
|
} else {
|
||||||
this.selectedItem.valueHasMutated();
|
this.autoSelect();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.selectedItem(null);
|
|
||||||
}
|
|
||||||
} else if (this.autoSelect() && this.focusedItem()) {
|
|
||||||
this.selectedItem(this.focusedItem());
|
|
||||||
}
|
}
|
||||||
}, this);
|
});
|
||||||
|
|
||||||
this.selectedItem.subscribe((item) => {
|
let selectedItemUseCallback = true;
|
||||||
|
|
||||||
|
koSelectedItem.subscribe(item => {
|
||||||
if (item) {
|
if (item) {
|
||||||
if (this.isListChecked()) {
|
// koList.forEach(subItem => subItem.checked(false));
|
||||||
this.listChecked().forEach(subItem => subItem.checked(false));
|
selectedItemUseCallback && itemSelectedThrottle(item);
|
||||||
}
|
} else {
|
||||||
|
selectedItemUseCallback && itemSelected();
|
||||||
if (this.selectedItemUseCallback) {
|
|
||||||
itemSelectedThrottle(item);
|
|
||||||
}
|
|
||||||
} else if (this.selectedItemUseCallback) {
|
|
||||||
this.itemSelected(null);
|
|
||||||
}
|
}
|
||||||
}, this);
|
});
|
||||||
|
|
||||||
this.selectedItem = this.selectedItem.extend({ toggleSubscribeProperty: [this, 'selected'] });
|
koFocusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)));
|
||||||
this.focusedItem = this.focusedItem.extend({ toggleSubscribeProperty: [null, 'focused'] });
|
|
||||||
|
|
||||||
this.focusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)), this);
|
/**
|
||||||
|
* Below code is used to keep checked/focused/selected states when array is refreshed.
|
||||||
|
*/
|
||||||
|
|
||||||
let aCache = [],
|
let aCheckedCache = [],
|
||||||
aCheckedCache = [],
|
|
||||||
mFocused = null,
|
mFocused = null,
|
||||||
mSelected = null;
|
mSelected = null;
|
||||||
|
|
||||||
this.list.subscribe(
|
// Before removing old list
|
||||||
|
koList.subscribe(
|
||||||
items => {
|
items => {
|
||||||
if (isArray(items)) {
|
if (isArray(items)) {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (item) {
|
const uid = this.getItemUid(item);
|
||||||
const uid = this.getItemUid(item);
|
if (uid) {
|
||||||
|
|
||||||
aCache.push(uid);
|
|
||||||
item.checked() && aCheckedCache.push(uid);
|
item.checked() && aCheckedCache.push(uid);
|
||||||
if (null === mFocused && item.focused()) {
|
if (!mFocused && item.focused()) {
|
||||||
mFocused = uid;
|
mFocused = uid;
|
||||||
}
|
}
|
||||||
if (null === mSelected && item.selected()) {
|
if (!mSelected && item.selected()) {
|
||||||
mSelected = uid;
|
mSelected = uid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,120 +111,64 @@ export class Selector {
|
||||||
'beforeChange'
|
'beforeChange'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.list.subscribe((aItems) => {
|
koList.subscribe(aItems => {
|
||||||
let temp = null,
|
selectedItemUseCallback = false;
|
||||||
getNext = false,
|
|
||||||
isNextFocused = mFocused,
|
|
||||||
isChecked = false,
|
|
||||||
isSelected = false,
|
|
||||||
len = 0;
|
|
||||||
|
|
||||||
const uids = [];
|
this.unselect();
|
||||||
|
|
||||||
this.selectedItemUseCallback = false;
|
|
||||||
|
|
||||||
this.focusedItem(null);
|
|
||||||
this.selectedItem(null);
|
|
||||||
|
|
||||||
if (isArray(aItems)) {
|
if (isArray(aItems)) {
|
||||||
len = aCheckedCache.length;
|
let temp,
|
||||||
|
isChecked,
|
||||||
|
next = this.iFocusedNextHelper || this.iSelectNextHelper;
|
||||||
|
|
||||||
aItems.forEach(item => {
|
aItems.forEach(item => {
|
||||||
const uid = this.getItemUid(item);
|
const uid = this.getItemUid(item);
|
||||||
uids.push(uid);
|
if (uid) {
|
||||||
|
|
||||||
if (null !== mFocused && mFocused === uid) {
|
if (mFocused === uid) {
|
||||||
this.focusedItem(item);
|
koFocusedItem(item);
|
||||||
mFocused = null;
|
mFocused = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (0 < len && aCheckedCache.includes(uid)) {
|
if (aCheckedCache.includes(uid)) {
|
||||||
isChecked = true;
|
item.checked(true);
|
||||||
item.checked(true);
|
isChecked = true;
|
||||||
--len;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!isChecked && null !== mSelected && mSelected === uid) {
|
if (!isChecked && mSelected === uid) {
|
||||||
isSelected = true;
|
koSelectedItem(item);
|
||||||
this.selectedItem(item);
|
mSelected = null;
|
||||||
mSelected = null;
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.selectedItemUseCallback = true;
|
selectedItemUseCallback = true;
|
||||||
|
|
||||||
if (!isChecked && !isSelected && this.autoSelect()) {
|
|
||||||
if (this.focusedItem()) {
|
|
||||||
this.selectedItem(this.focusedItem());
|
|
||||||
} else if (aItems.length) {
|
|
||||||
if (null !== isNextFocused) {
|
|
||||||
getNext = false;
|
|
||||||
isNextFocused = aCache.find(sUid => {
|
|
||||||
if (getNext && uids.includes(sUid)) {
|
|
||||||
return sUid;
|
|
||||||
}
|
|
||||||
if (isNextFocused === sUid) {
|
|
||||||
getNext = true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isNextFocused) {
|
|
||||||
temp = aItems.find(oItem => isNextFocused === this.getItemUid(oItem));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedItem(temp || null);
|
|
||||||
this.focusedItem(this.selectedItem());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(0 !== this.iSelectNextHelper || 0 !== this.iFocusedNextHelper) &&
|
|
||||||
aItems.length &&
|
|
||||||
!this.focusedItem()
|
|
||||||
) {
|
|
||||||
temp = null;
|
|
||||||
if (0 !== this.iFocusedNextHelper) {
|
|
||||||
temp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!temp && 0 !== this.iSelectNextHelper) {
|
|
||||||
temp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (next && aItems.length && !koFocusedItem()) {
|
||||||
|
temp = aItems[-1 === next ? aItems.length - 1 : 0];
|
||||||
if (temp) {
|
if (temp) {
|
||||||
if (0 !== this.iSelectNextHelper) {
|
this.iSelectNextHelper && koSelectedItem(temp);
|
||||||
this.selectedItem(temp || null);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusedItem(temp || null);
|
koFocusedItem(temp);
|
||||||
|
|
||||||
this.scrollToFocused();
|
this.scrollToFocused();
|
||||||
|
|
||||||
setTimeout(this.scrollToFocused, 100);
|
setTimeout(this.scrollToFocused, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.iSelectNextHelper = 0;
|
this.iSelectNextHelper = 0;
|
||||||
this.iFocusedNextHelper = 0;
|
this.iFocusedNextHelper = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
!isChecked && !koSelectedItem() && this.autoSelect();
|
||||||
}
|
}
|
||||||
|
|
||||||
aCache = [];
|
|
||||||
aCheckedCache = [];
|
aCheckedCache = [];
|
||||||
mFocused = null;
|
mFocused = null;
|
||||||
mSelected = null;
|
mSelected = null;
|
||||||
|
selectedItemUseCallback = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
itemSelected(item) {
|
|
||||||
if (this.isListChecked()) {
|
|
||||||
item || (this.oCallbacks.ItemSelect || (()=>0))(null);
|
|
||||||
} else if (item) {
|
|
||||||
(this.oCallbacks.ItemSelect || (()=>0))(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unselect() {
|
unselect() {
|
||||||
this.selectedItem(null);
|
this.selectedItem(null);
|
||||||
this.focusedItem(null);
|
this.focusedItem(null);
|
||||||
|
|
@ -239,32 +183,34 @@ export class Selector {
|
||||||
return el ? ko.dataFor(el) : null;
|
return el ? ko.dataFor(el) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
contentScrollable.addEventListener('click', event => {
|
addEventsListeners(contentScrollable, {
|
||||||
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
click: event => {
|
||||||
el && this.actionClick(ko.dataFor(el), event);
|
const el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||||
|
let item = el && ko.dataFor(el);
|
||||||
|
el && (this.oCallbacks.click || (()=>1))(event, item) && this.actionClick(item, event);
|
||||||
|
|
||||||
const item = getItem(this.sItemCheckedSelector);
|
item = getItem(this.sItemCheckedSelector);
|
||||||
if (item) {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
this.actionClick(item, event);
|
|
||||||
} else {
|
|
||||||
this.focusedItem(item);
|
|
||||||
item.checked(!item.checked());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
contentScrollable.addEventListener('auxclick', event => {
|
|
||||||
if (1 == event.button) {
|
|
||||||
const item = getItem(this.sItemSelector);
|
|
||||||
if (item) {
|
if (item) {
|
||||||
this.focusedItem(item);
|
if (event.shiftKey) {
|
||||||
(this.oCallbacks.MiddleClick || (()=>0))(item);
|
this.actionClick(item, event);
|
||||||
|
} else {
|
||||||
|
this.focusedItem(item);
|
||||||
|
item.checked(!item.checked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auxclick: event => {
|
||||||
|
if (1 == event.button) {
|
||||||
|
const item = getItem(this.sItemSelector);
|
||||||
|
if (item) {
|
||||||
|
this.focusedItem(item);
|
||||||
|
this.oCallbacks.MiddleClick?.(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
shortcuts.add('enter,open', '', keyScope, () => {
|
registerShortcut('enter,open', '', keyScope, () => {
|
||||||
const focused = this.focusedItem();
|
const focused = this.focusedItem();
|
||||||
if (focused && !focused.selected()) {
|
if (focused && !focused.selected()) {
|
||||||
this.actionClick(focused);
|
this.actionClick(focused);
|
||||||
|
|
@ -272,13 +218,13 @@ export class Selector {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
shortcuts.add('arrowup,arrowdown', 'meta', keyScope, () => false);
|
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
|
||||||
|
|
||||||
shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => {
|
addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
|
||||||
this.newSelectPosition(event.key, true);
|
this.newSelectPosition(event.key, true);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
||||||
this.newSelectPosition(event.key, false);
|
this.newSelectPosition(event.key, false);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
@ -288,8 +234,10 @@ export class Selector {
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
autoSelect() {
|
autoSelect(bForce) {
|
||||||
return !!(this.oCallbacks.AutoSelect || (()=>1))();
|
(bForce || (this.oCallbacks.canSelect || (()=>1))())
|
||||||
|
&& this.focusedItem()
|
||||||
|
&& this.selectedItem(this.focusedItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -297,14 +245,7 @@ export class Selector {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getItemUid(item) {
|
getItemUid(item) {
|
||||||
let uid = '';
|
return (item && this.oCallbacks.ItemGetUid?.(item)?.toString()) || '';
|
||||||
|
|
||||||
const getItemUidCallback = this.oCallbacks.ItemGetUid || null;
|
|
||||||
if (getItemUidCallback && item) {
|
|
||||||
uid = getItemUidCallback(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return uid.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -313,24 +254,36 @@ export class Selector {
|
||||||
* @param {boolean=} bForceSelect = false
|
* @param {boolean=} bForceSelect = false
|
||||||
*/
|
*/
|
||||||
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
|
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
|
||||||
let isArrow = 'ArrowUp' === sEventKey || 'ArrowDown' === sEventKey,
|
let result;
|
||||||
result = null;
|
|
||||||
|
|
||||||
const pageStep = 10,
|
const up = 'ArrowUp' === sEventKey,
|
||||||
|
isArrow = up || 'ArrowDown' === sEventKey,
|
||||||
|
pageStep = 10,
|
||||||
list = this.list(),
|
list = this.list(),
|
||||||
listLen = list.length,
|
listLen = list.length,
|
||||||
focused = this.focusedItem();
|
focused = this.focusedItem();
|
||||||
|
|
||||||
if (listLen) {
|
bShiftKey || (shiftStart = -1);
|
||||||
|
|
||||||
|
if (' ' === sEventKey) {
|
||||||
|
focused?.checked(!focused.checked());
|
||||||
|
} else if (listLen) {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
if (isArrow) {
|
if (isArrow) {
|
||||||
let i = list.indexOf(focused);
|
let i = list.indexOf(focused);
|
||||||
if ('ArrowUp' == sEventKey) {
|
if (bShiftKey) {
|
||||||
i > 0 && (result = list[i-1]);
|
shiftStart = -1 < shiftStart ? shiftStart : i;
|
||||||
|
shiftStart == i
|
||||||
|
? focused.checked(true)
|
||||||
|
: ((up ? shiftStart < i : shiftStart > i) && focused.checked(false));
|
||||||
|
}
|
||||||
|
if (up) {
|
||||||
|
i > 0 && (result = list[--i]);
|
||||||
} else if (++i < listLen) {
|
} else if (++i < listLen) {
|
||||||
result = list[i];
|
result = list[i];
|
||||||
}
|
}
|
||||||
result || (this.oCallbacks.UpOrDown || (()=>0))('ArrowUp' === sEventKey);
|
bShiftKey && result?.checked(true);
|
||||||
|
result || this.oCallbacks.UpOrDown?.(up);
|
||||||
} else if ('Home' === sEventKey) {
|
} else if ('Home' === sEventKey) {
|
||||||
result = list[0];
|
result = list[0];
|
||||||
} else if ('End' === sEventKey) {
|
} else if ('End' === sEventKey) {
|
||||||
|
|
@ -357,26 +310,12 @@ export class Selector {
|
||||||
) {
|
) {
|
||||||
result = list[list.length - 1];
|
result = list[list.length - 1];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this.focusedItem(result);
|
this.focusedItem(result);
|
||||||
|
!this.list.hasChecked() && this.autoSelect(bForceSelect);
|
||||||
if (focused && ((bShiftKey && isArrow) || ' ' === sEventKey)) {
|
this.scrollToFocused();
|
||||||
focused.checked(!focused.checked());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (' ' !== sEventKey && (this.autoSelect() || bForceSelect) && !this.isListChecked()) {
|
|
||||||
this.selectedItem(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scrollToFocused();
|
|
||||||
} else if (focused) {
|
|
||||||
if ((bShiftKey && isArrow) || ' ' === sEventKey) {
|
|
||||||
focused.checked(!focused.checked());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusedItem(focused);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,16 +325,15 @@ export class Selector {
|
||||||
scrollToFocused() {
|
scrollToFocused() {
|
||||||
const scrollable = this.oContentScrollable;
|
const scrollable = this.oContentScrollable;
|
||||||
if (scrollable) {
|
if (scrollable) {
|
||||||
let block, focused = scrollable.querySelector(this.sItemFocusedSelector);
|
let focused = scrollable.querySelector(this.sItemFocusedSelector);
|
||||||
if (focused) {
|
if (focused) {
|
||||||
const fRect = focused.getBoundingClientRect(),
|
const fRect = focused.getBoundingClientRect(),
|
||||||
sRect = scrollable.getBoundingClientRect();
|
sRect = scrollable.getBoundingClientRect();
|
||||||
if (fRect.top < sRect.top) {
|
if (fRect.top < sRect.top) {
|
||||||
block = 'start';
|
focused.scrollIntoView(true);
|
||||||
} else if (fRect.bottom > sRect.bottom) {
|
} else if (fRect.bottom > sRect.bottom) {
|
||||||
block = 'end';
|
focused.scrollIntoView(false);
|
||||||
}
|
}
|
||||||
block && focused.scrollIntoView(block === 'start');
|
|
||||||
} else {
|
} else {
|
||||||
scrollable.scrollTop = 0;
|
scrollable.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -409,78 +347,47 @@ export class Selector {
|
||||||
this.oContentScrollable && (this.oContentScrollable.scrollTop = 0);
|
this.oContentScrollable && (this.oContentScrollable.scrollTop = 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
eventClickFunction(item, event) {
|
|
||||||
let index = 0,
|
|
||||||
length = 0,
|
|
||||||
changeRange = false,
|
|
||||||
isInRange = false,
|
|
||||||
list = [],
|
|
||||||
checked = false,
|
|
||||||
listItem = null,
|
|
||||||
lineUid = '';
|
|
||||||
|
|
||||||
const uid = this.getItemUid(item);
|
|
||||||
if (event && event.shiftKey) {
|
|
||||||
if (uid && this.sLastUid && uid !== this.sLastUid) {
|
|
||||||
list = this.list();
|
|
||||||
checked = item.checked();
|
|
||||||
|
|
||||||
for (index = 0, length = list.length; index < length; index++) {
|
|
||||||
listItem = list[index];
|
|
||||||
lineUid = this.getItemUid(listItem);
|
|
||||||
|
|
||||||
changeRange = false;
|
|
||||||
if (lineUid === this.sLastUid || lineUid === uid) {
|
|
||||||
changeRange = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changeRange) {
|
|
||||||
isInRange = !isInRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInRange || changeRange) {
|
|
||||||
listItem.checked(checked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sLastUid = uid || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} item
|
* @param {Object} item
|
||||||
* @param {Object=} event
|
* @param {Object=} event
|
||||||
*/
|
*/
|
||||||
actionClick(item, event = null) {
|
actionClick(item, event) {
|
||||||
if (item) {
|
if (item) {
|
||||||
let click = true;
|
let select = true;
|
||||||
if (event) {
|
if (event && !event.altKey) {
|
||||||
if (event.shiftKey && !(event.ctrlKey || event.metaKey) && !event.altKey) {
|
if (event.shiftKey && !event.ctrlKey && !event.metaKey) {
|
||||||
click = false;
|
const uid = this.getItemUid(item);
|
||||||
if (!this.sLastUid) {
|
if (uid && this.sLastUid && uid !== this.sLastUid) {
|
||||||
this.sLastUid = this.getItemUid(item);
|
let changeRange = false,
|
||||||
|
isInRange = false,
|
||||||
|
checked = !item.checked(),
|
||||||
|
lineUid = '';
|
||||||
|
this.list().forEach(listItem => {
|
||||||
|
lineUid = this.getItemUid(listItem);
|
||||||
|
changeRange = (lineUid === this.sLastUid || lineUid === uid);
|
||||||
|
if (isInRange || changeRange) {
|
||||||
|
if (changeRange) {
|
||||||
|
isInRange = !isInRange;
|
||||||
|
}
|
||||||
|
listItem.checked(checked);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
this.sLastUid = uid;
|
||||||
item.checked(!item.checked());
|
|
||||||
this.eventClickFunction(item, event);
|
|
||||||
|
|
||||||
this.focusedItem(item);
|
this.focusedItem(item);
|
||||||
} else if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
return;
|
||||||
click = false;
|
}
|
||||||
|
if (!event.shiftKey && (event.ctrlKey || event.metaKey)) {
|
||||||
|
select = false;
|
||||||
this.focusedItem(item);
|
this.focusedItem(item);
|
||||||
|
const selected = this.selectedItem();
|
||||||
if (this.selectedItem() && item !== this.selectedItem()) {
|
if (selected && item !== selected) {
|
||||||
this.selectedItem().checked(true);
|
selected.checked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
item.checked(!item.checked());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (click) {
|
select ? this.selectMessageItem(item) : item.checked(!item.checked());
|
||||||
this.selectMessageItem(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,19 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { Notification, UploadErrorCode } from 'Common/Enums';
|
import { Notifications, UploadErrorCode } from 'Common/Enums';
|
||||||
import { langLink } from 'Common/Links';
|
import { langLink } from 'Common/Links';
|
||||||
import { doc, createElement } from 'Common/Globals';
|
import { doc, createElement } from 'Common/Globals';
|
||||||
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
||||||
|
import { pInt } from 'Common/Utils';
|
||||||
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
|
||||||
let I18N_DATA = {};
|
let I18N_DATA = {};
|
||||||
|
|
||||||
const
|
const
|
||||||
i18nToNode = element => {
|
|
||||||
const key = element.dataset.i18n;
|
|
||||||
if (key) {
|
|
||||||
if ('[' === key.slice(0, 1)) {
|
|
||||||
switch (key.slice(0, 6)) {
|
|
||||||
case '[html]':
|
|
||||||
element.innerHTML = i18n(key.slice(6));
|
|
||||||
break;
|
|
||||||
case '[place':
|
|
||||||
element.placeholder = i18n(key.slice(13));
|
|
||||||
break;
|
|
||||||
case '[title':
|
|
||||||
element.title = i18n(key.slice(7));
|
|
||||||
break;
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
element.textContent = i18n(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
init = () => {
|
init = () => {
|
||||||
if (rl.I18N) {
|
if (rl.I18N) {
|
||||||
I18N_DATA = rl.I18N;
|
I18N_DATA = rl.I18N;
|
||||||
Date.defineRelativeTimeFormat(rl.relativeTime || {});
|
|
||||||
rl.I18N = null;
|
rl.I18N = null;
|
||||||
|
doc.documentElement.dir = I18N_DATA.LANG_DIR;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -41,15 +21,38 @@ const
|
||||||
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
||||||
|
|
||||||
getNotificationMessage = code => {
|
getNotificationMessage = code => {
|
||||||
let key = getKeyByValue(Notification, code);
|
let key = getKeyByValue(Notifications, code);
|
||||||
if (key) {
|
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
|
||||||
key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
|
},
|
||||||
return I18N_DATA.NOTIFICATIONS[key];
|
|
||||||
}
|
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
||||||
};
|
|
||||||
|
|
||||||
export const
|
export const
|
||||||
trigger = ko.observable(false),
|
translateTrigger = ko.observable(false),
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
|
||||||
|
// see /snappymail/v/0.0.0/app/localization/relativetimeformat/
|
||||||
|
relativeTime = seconds => {
|
||||||
|
let unit = 'second',
|
||||||
|
t = [[60,'minute'],[3600,'hour'],[86400,'day'],[2628000,'month'],[31536000,'year']],
|
||||||
|
i = 5,
|
||||||
|
abs = Math.abs(seconds);
|
||||||
|
while (i--) {
|
||||||
|
if (t[i][0] <= abs) {
|
||||||
|
seconds = Math.round(seconds / t[i][0]);
|
||||||
|
unit = t[i][1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Intl.RelativeTimeFormat) {
|
||||||
|
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
|
||||||
|
}
|
||||||
|
// Safari < 14
|
||||||
|
abs = Math.abs(seconds);
|
||||||
|
let rtf = rl.relativeTime.long[unit][0 > seconds ? 'past' : 'future'],
|
||||||
|
plural = rl.relativeTime.plural(abs);
|
||||||
|
return (rtf[plural] || rtf).replace('{0}', abs);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} key
|
* @param {string} key
|
||||||
|
|
@ -58,18 +61,14 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
i18n = (key, valueList, defaulValue) => {
|
i18n = (key, valueList, defaulValue) => {
|
||||||
let result = defaulValue || key;
|
let result = defaulValue ?? key;
|
||||||
if (key) {
|
let path = key.split('/');
|
||||||
let path = key.split('/');
|
if (I18N_DATA[path[0]] && path[1]) {
|
||||||
if (I18N_DATA[path[0]] && path[1]) {
|
result = I18N_DATA[path[0]][path[1]] || result;
|
||||||
result = I18N_DATA[path[0]][path[1]] || result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (valueList) {
|
|
||||||
forEachObjectEntry(valueList, (key, value) => {
|
|
||||||
result = result.replace('%' + key + '%', value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
valueList && forEachObjectEntry(valueList, (key, value) => {
|
||||||
|
result = result.replace('%' + key + '%', value);
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -79,17 +78,93 @@ export const
|
||||||
*/
|
*/
|
||||||
i18nToNodes = element =>
|
i18nToNodes = element =>
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
|
element.querySelectorAll('[data-i18n]').forEach(element => {
|
||||||
|
const key = element.dataset.i18n;
|
||||||
|
if ('[' === key[0]) {
|
||||||
|
switch (key.slice(1, 6)) {
|
||||||
|
case 'html]':
|
||||||
|
element.innerHTML = i18n(key.slice(6));
|
||||||
|
break;
|
||||||
|
case 'place':
|
||||||
|
element.placeholder = i18n(key.slice(13));
|
||||||
|
break;
|
||||||
|
case 'title':
|
||||||
|
element.title = i18n(key.slice(7));
|
||||||
|
break;
|
||||||
|
// no default
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
element.textContent = i18n(key);
|
||||||
|
}
|
||||||
|
})
|
||||||
, 1),
|
, 1),
|
||||||
|
|
||||||
|
timestampToString = (timeStampInUTC, formatStr) => {
|
||||||
|
const now = Date.now(),
|
||||||
|
time = 0 < timeStampInUTC ? timeStampInUTC * 1000 : (0 === timeStampInUTC ? now : 0);
|
||||||
|
if (31536000000 < time) {
|
||||||
|
const m = new Date(time), h = LanguageStore.hourCycle();
|
||||||
|
switch (formatStr) {
|
||||||
|
case 'FROMNOW':
|
||||||
|
return fromNow(m);
|
||||||
|
case 'AUTO': {
|
||||||
|
// 4 hours
|
||||||
|
if (14400000 >= now - time)
|
||||||
|
return fromNow(m);
|
||||||
|
const date = new Date,
|
||||||
|
dt = date.setHours(0,0,0,0);
|
||||||
|
return (time > dt - 86400000)
|
||||||
|
? i18n(
|
||||||
|
time > dt ? 'MESSAGE_LIST/TODAY_AT' : 'MESSAGE_LIST/YESTERDAY_AT',
|
||||||
|
{TIME: m.format('LT',0,h)}
|
||||||
|
)
|
||||||
|
: m.format(
|
||||||
|
date.getFullYear() === m.getFullYear()
|
||||||
|
? {day: '2-digit', month: 'short', hour: 'numeric', minute: 'numeric'}
|
||||||
|
: {dateStyle: 'medium', timeStyle: 'short'}
|
||||||
|
, 0, h);
|
||||||
|
}
|
||||||
|
case 'FULL':
|
||||||
|
return m.format('LLL',0,h);
|
||||||
|
default:
|
||||||
|
return m.format(formatStr,0,h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
timeToNode = (element, time) => {
|
||||||
|
try {
|
||||||
|
if (time) {
|
||||||
|
element.dateTime = new Date(time * 1000).toISOString();
|
||||||
|
} else {
|
||||||
|
time = Date.parse(element.dateTime) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = element.dataset.timeFormat;
|
||||||
|
if (key) {
|
||||||
|
element.textContent = timestampToString(time, key);
|
||||||
|
if ('FULL' !== key && 'FROMNOW' !== key) {
|
||||||
|
element.title = timestampToString(time, 'FULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// prevent knockout crashes
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reloadTime = () => doc.querySelectorAll('time').forEach(element => timeToNode(element)),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} startCallback
|
* @param {Function} startCallback
|
||||||
* @param {Function=} langCallback = null
|
* @param {Function=} langCallback = null
|
||||||
*/
|
*/
|
||||||
initOnStartOrLangChange = (startCallback, langCallback = null) => {
|
initOnStartOrLangChange = (startCallback, langCallback) => {
|
||||||
startCallback && startCallback();
|
startCallback?.();
|
||||||
startCallback && trigger.subscribe(startCallback);
|
startCallback && translateTrigger.subscribe(startCallback);
|
||||||
langCallback && trigger.subscribe(langCallback);
|
langCallback && translateTrigger.subscribe(langCallback);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -99,16 +174,19 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getNotification = (code, message = '', defCode = 0) => {
|
getNotification = (code, message = '', defCode = 0) => {
|
||||||
code = parseInt(code, 10) || 0;
|
code = pInt(code);
|
||||||
if (Notification.ClientViewError === code && message) {
|
if (Notifications.ClientViewError === code && message) {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getNotificationMessage(code)
|
return getNotificationMessage(code)
|
||||||
|| getNotificationMessage(parseInt(defCode, 10))
|
|| getNotificationMessage(pInt(defCode))
|
||||||
|| '';
|
|| '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getErrorMessage = (code, data) =>
|
||||||
|
getNotification(code) || data?.messageAdditional || data?.message || data,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {*} code
|
* @param {*} code
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
|
@ -122,27 +200,26 @@ export const
|
||||||
* @param {boolean} admin
|
* @param {boolean} admin
|
||||||
* @param {string} language
|
* @param {string} language
|
||||||
*/
|
*/
|
||||||
translatorReload = (admin, language) =>
|
translatorReload = (language, admin) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const script = createElement('script');
|
const script = createElement('script');
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
// reload the data
|
// reload the data
|
||||||
if (init()) {
|
if (init()) {
|
||||||
i18nToNodes(doc);
|
i18nToNodes(doc);
|
||||||
admin || rl.app.reloadTime();
|
translateTrigger(!translateTrigger());
|
||||||
trigger(!trigger());
|
// admin || reloadTime();
|
||||||
}
|
}
|
||||||
script.remove();
|
script.remove();
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
script.onerror = () => reject(Error('Language '+language+' failed'));
|
||||||
script.src = langLink(language, admin);
|
script.src = langLink(language, admin);
|
||||||
// script.async = true;
|
// script.async = true;
|
||||||
doc.head.append(script);
|
doc.head.append(script);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {string} language
|
* @param {string} language
|
||||||
* @param {boolean=} isEng = false
|
* @param {boolean=} isEng = false
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
|
@ -152,6 +229,8 @@ export const
|
||||||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
||||||
null,
|
null,
|
||||||
language
|
language
|
||||||
);
|
),
|
||||||
|
|
||||||
|
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
|
||||||
import { doc, elementById } from 'Common/Globals';
|
|
||||||
|
|
||||||
let __themeTimer = 0,
|
|
||||||
__themeJson = null;
|
|
||||||
|
|
||||||
export const
|
export const
|
||||||
isArray = Array.isArray,
|
isArray = Array.isArray,
|
||||||
arrayLength = array => isArray(array) && array.length,
|
arrayLength = array => isArray(array) && array.length,
|
||||||
|
|
@ -16,96 +10,22 @@ export const
|
||||||
|
|
||||||
pInt = (value, defaultValue = 0) => {
|
pInt = (value, defaultValue = 0) => {
|
||||||
value = parseInt(value, 10);
|
value = parseInt(value, 10);
|
||||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
return isFinite(value) ? value : defaultValue;
|
||||||
},
|
},
|
||||||
|
|
||||||
convertThemeName = theme => theme
|
|
||||||
.replace(/@custom$/, '')
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
||||||
.trim(),
|
|
||||||
|
|
||||||
defaultOptionsAfterRender = (domItem, item) =>
|
defaultOptionsAfterRender = (domItem, item) =>
|
||||||
domItem && item && undefined !== item.disabled
|
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
|
||||||
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
|
|
||||||
|
|
||||||
addObservablesTo = (target, observables) =>
|
|
||||||
forEachObjectEntry(observables, (key, value) =>
|
|
||||||
target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) ),
|
|
||||||
|
|
||||||
addComputablesTo = (target, computables) =>
|
|
||||||
forEachObjectEntry(computables, (key, fn) => target[key] = ko.computed(fn, {'pure':true})),
|
|
||||||
|
|
||||||
addSubscribablesTo = (target, subscribables) =>
|
|
||||||
forEachObjectEntry(subscribables, (key, fn) => target[key].subscribe(fn)),
|
|
||||||
|
|
||||||
inFocus = () => {
|
|
||||||
try {
|
|
||||||
return doc.activeElement && doc.activeElement.matches(
|
|
||||||
'input,textarea,[contenteditable]'
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
||||||
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
|
b64Encode = data => btoa(unescape(encodeURIComponent(data))),
|
||||||
/* // Without deprecated 'unescape':
|
/* // Without deprecated 'unescape':
|
||||||
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
|
b64Encode = data => btoa(encodeURIComponent(data).replace(
|
||||||
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
||||||
)),
|
)),
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
|
||||||
|
|
||||||
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
|
|
||||||
settingsSaveHelperSimpleFunction = (koTrigger, context) =>
|
|
||||||
iError => {
|
|
||||||
koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
|
|
||||||
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
changeTheme = (value, themeTrigger = ()=>0) => {
|
|
||||||
const themeStyle = elementById('app-theme-style'),
|
|
||||||
clearTimer = () => {
|
|
||||||
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
|
|
||||||
__themeJson = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = themeStyle.dataset.href;
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
url = url.toString()
|
|
||||||
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
|
|
||||||
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
|
|
||||||
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
|
|
||||||
|
|
||||||
if ('Json/' !== url.substr(-5)) {
|
|
||||||
url += 'Json/';
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(__themeTimer);
|
|
||||||
|
|
||||||
themeTrigger(SaveSettingsStep.Animate);
|
|
||||||
|
|
||||||
if (__themeJson) {
|
|
||||||
__themeJson.abort();
|
|
||||||
}
|
|
||||||
let init = {};
|
|
||||||
if (window.AbortController) {
|
|
||||||
__themeJson = new AbortController();
|
|
||||||
init.signal = __themeJson.signal;
|
|
||||||
}
|
|
||||||
rl.fetchJSON(url, init)
|
|
||||||
.then(data => {
|
|
||||||
if (2 === arrayLength(data)) {
|
|
||||||
themeStyle.textContent = data[1];
|
|
||||||
themeStyle.dataset.href = url;
|
|
||||||
themeStyle.dataset.theme = data[0];
|
|
||||||
themeTrigger(SaveSettingsStep.TrueResult);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(clearTimer, clearTimer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,66 @@
|
||||||
import { ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
import { Notifications } from 'Common/Enums';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||||
import { isArray } from 'Common/Utils';
|
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { doc, createElement } from 'Common/Globals';
|
import { plainToHtml } from 'Common/Html';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { getNotification } from 'Common/Translator';
|
||||||
|
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||||
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import * as Local from 'Storage/Client';
|
import * as Local from 'Storage/Client';
|
||||||
import { plainToHtml } from 'Common/Html';
|
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
import { attachmentDownload } from 'Common/Links';
|
||||||
|
|
||||||
|
import { AccountModel } from 'Model/Account';
|
||||||
|
import { IdentityModel } from 'Model/Identity';
|
||||||
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
|
import { isArray } from 'Common/Utils';
|
||||||
|
|
||||||
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
||||||
sortFolders = folders => {
|
// 1 = move, 2 = copy
|
||||||
try {
|
moveAction = ko.observable(0),
|
||||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
|
||||||
folders.sort((a, b) =>
|
dropdownsDetectVisibility = (() =>
|
||||||
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||||
);
|
).debounce(50),
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
editIdentity = Identity => showScreenPopup(IdentityPopupView, [Identity]),
|
||||||
|
|
||||||
|
loadAccountsAndIdentities = () => {
|
||||||
|
AccountUserStore.loading(true);
|
||||||
|
IdentityUserStore.loading(true);
|
||||||
|
|
||||||
|
Remote.request('AccountsAndIdentities', (iError, oData) => {
|
||||||
|
AccountUserStore.loading(false);
|
||||||
|
IdentityUserStore.loading(false);
|
||||||
|
|
||||||
|
if (!iError) {
|
||||||
|
let items = oData.Result.Accounts;
|
||||||
|
AccountUserStore(isArray(items)
|
||||||
|
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
AccountUserStore.unshift(new AccountModel(SettingsGet('mainEmail'), '', false));
|
||||||
|
|
||||||
|
items = oData.Result.Identities;
|
||||||
|
IdentityUserStore(isArray(items)
|
||||||
|
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invoke "Update Identity" pop up right after login
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1689
|
||||||
|
const main = IdentityUserStore.main();
|
||||||
|
main && !main.exists() && setTimeout(()=>editIdentity(main), 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,91 +68,43 @@ sortFolders = folders => {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
download = (link, name = "") => {
|
download = (link, name = "") => {
|
||||||
if (ThemeStore.isMobile()) {
|
console.log('download: '+link);
|
||||||
open(link, '_self');
|
// Firefox 98 issue https://github.com/the-djmaze/snappymail/issues/301
|
||||||
|
if (ThemeStore.isMobile() || /firefox/i.test(navigator.userAgent)) {
|
||||||
|
open(link, '_blank');
|
||||||
focus();
|
focus();
|
||||||
} else {
|
} else {
|
||||||
const oLink = createElement('a');
|
const oLink = createElement('a', {
|
||||||
oLink.href = link;
|
href: link,
|
||||||
oLink.target = '_blank';
|
target: '_blank',
|
||||||
oLink.download = name;
|
download: name
|
||||||
|
});
|
||||||
doc.body.appendChild(oLink).click();
|
doc.body.appendChild(oLink).click();
|
||||||
oLink.remove();
|
oLink.remove();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
downloadZip = (name, hashes, onError, fTrigger, folder) => {
|
||||||
* @param {Array=} aDisabled
|
if (hashes.length) {
|
||||||
* @param {Array=} aHeaderLines
|
let params = {
|
||||||
* @param {Function=} fDisableCallback
|
target: 'zip',
|
||||||
* @param {Function=} fRenameCallback
|
filename: name,
|
||||||
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
|
hashes: hashes
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
folderListOptionsBuilder = (
|
|
||||||
aDisabled,
|
|
||||||
aHeaderLines,
|
|
||||||
fRenameCallback,
|
|
||||||
fDisableCallback,
|
|
||||||
bNoSelectSelectable,
|
|
||||||
aList = FolderUserStore.folderList()
|
|
||||||
) => {
|
|
||||||
const
|
|
||||||
aResult = [],
|
|
||||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
|
||||||
// FolderSystemPopupView should always be true
|
|
||||||
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
|
||||||
|
|
||||||
foldersWalk = folders => {
|
|
||||||
folders.forEach(oItem => {
|
|
||||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
|
||||||
aResult.push({
|
|
||||||
id: oItem.fullName,
|
|
||||||
name:
|
|
||||||
sDeepPrefix.repeat(oItem.deep) +
|
|
||||||
fRenameCallback(oItem),
|
|
||||||
system: false,
|
|
||||||
disabled: !bNoSelectSelectable && (
|
|
||||||
!oItem.selectable() ||
|
|
||||||
aDisabled.includes(oItem.fullName) ||
|
|
||||||
fDisableCallback(oItem))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oItem.subFolders.length) {
|
|
||||||
foldersWalk(oItem.subFolders());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
if (!onError) {
|
||||||
|
onError = () => alert('Download failed');
|
||||||
fDisableCallback = fDisableCallback || (() => false);
|
}
|
||||||
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
if (folder) {
|
||||||
isArray(aDisabled) || (aDisabled = []);
|
params.folder = folder;
|
||||||
|
// params.uids = uids;
|
||||||
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
}
|
||||||
aResult.push({
|
Remote.post('AttachmentsActions', fTrigger || null, params)
|
||||||
id: line[0],
|
.then(result => {
|
||||||
name: line[1],
|
let hash = result?.Result?.fileHash;
|
||||||
system: false,
|
hash ? download(attachmentDownload(hash), hash+'.zip') : onError();
|
||||||
disabled: false
|
|
||||||
})
|
})
|
||||||
);
|
.catch(onError);
|
||||||
|
}
|
||||||
foldersWalk(aList);
|
|
||||||
|
|
||||||
return aResult;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
|
|
||||||
* @param {Object|Array} objectOrObjects
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
delegateRunOnDestroy = (objectOrObjects) => {
|
|
||||||
objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
|
|
||||||
obj => obj.onDestroy && obj.onDestroy()
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -121,27 +115,24 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||||
const currentPage = koCurrentPage(),
|
const currentPage = koCurrentPage(),
|
||||||
pageCount = koPageCount(),
|
pageCount = koPageCount(),
|
||||||
result = [],
|
result = [],
|
||||||
|
lang = doc.documentElement.lang,
|
||||||
fAdd = (index, push = true, customName = '') => {
|
fAdd = (index, push = true, customName = '') => {
|
||||||
const data = {
|
const name = index.toLocaleString(lang),
|
||||||
current: index === currentPage,
|
data = {
|
||||||
name: customName ? customName.toString() : index.toString(),
|
current: index === currentPage,
|
||||||
custom: !!customName,
|
name: customName || name,
|
||||||
title: customName ? index.toString() : '',
|
title: customName ? name : '',
|
||||||
value: index.toString()
|
value: index
|
||||||
};
|
};
|
||||||
|
|
||||||
if (push) {
|
push ? result.push(data) : result.unshift(data);
|
||||||
result.push(data);
|
|
||||||
} else {
|
|
||||||
result.unshift(data);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let prev = 0,
|
let prev = 0,
|
||||||
next = 0,
|
next = 0,
|
||||||
limit = 2;
|
limit = 2;
|
||||||
|
|
||||||
if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) {
|
if (1 < pageCount) {
|
||||||
if (pageCount < currentPage) {
|
if (pageCount < currentPage) {
|
||||||
fAdd(pageCount);
|
fAdd(pageCount);
|
||||||
prev = pageCount;
|
prev = pageCount;
|
||||||
|
|
@ -203,56 +194,24 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||||
* @param {string} mailToUrl
|
* @param {string} mailToUrl
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
mailToHelper = (mailToUrl) => {
|
mailToHelper = mailToUrl => {
|
||||||
if (
|
if ('mailto:' === mailToUrl?.slice(0, 7).toLowerCase()) {
|
||||||
mailToUrl &&
|
mailToUrl = mailToUrl.slice(7).split('?');
|
||||||
'mailto:' ===
|
|
||||||
mailToUrl
|
|
||||||
.toString()
|
|
||||||
.slice(0, 7)
|
|
||||||
.toLowerCase()
|
|
||||||
) {
|
|
||||||
mailToUrl = mailToUrl.toString().slice(7);
|
|
||||||
|
|
||||||
let to = [],
|
const
|
||||||
params = {};
|
email = decodeURIComponent(mailToUrl[0]),
|
||||||
|
params = new URLSearchParams(mailToUrl[1]),
|
||||||
const email = mailToUrl.replace(/\?.+$/, ''),
|
to = params.get('to'),
|
||||||
query = mailToUrl.replace(/^[^?]*\?/, ''),
|
toEmailModel = value => EmailCollectionModel.fromString(value);
|
||||||
toEmailModel = value => null != value ? EmailModel.parseEmailLine(decodeURIComponent(value)) : null;
|
|
||||||
|
|
||||||
query.split('&').forEach(temp => {
|
|
||||||
temp = temp.split('=');
|
|
||||||
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (null != params.to) {
|
|
||||||
to = Object.values(
|
|
||||||
toEmailModel(email + ',' + params.to).reduce((result, value) => {
|
|
||||||
if (value) {
|
|
||||||
if (result[value.email]) {
|
|
||||||
if (!result[value.email].name) {
|
|
||||||
result[value.email] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result[value.email] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, {})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
to = EmailModel.parseEmailLine(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessageComposer([
|
showMessageComposer([
|
||||||
ComposeType.Empty,
|
ComposeType.Empty,
|
||||||
null,
|
null,
|
||||||
to,
|
toEmailModel(to ? email + ',' + to : email),
|
||||||
toEmailModel(params.cc),
|
toEmailModel(params.get('cc')),
|
||||||
toEmailModel(params.bcc),
|
toEmailModel(params.get('bcc')),
|
||||||
null == params.subject ? null : decodeURIComponent(params.subject),
|
params.get('subject'),
|
||||||
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
|
plainToHtml(params.get('body') || '')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -266,39 +225,27 @@ showMessageComposer = (params = []) =>
|
||||||
rl.app.showMessageComposer(params);
|
rl.app.showMessageComposer(params);
|
||||||
},
|
},
|
||||||
|
|
||||||
initFullscreen = (el, fn) =>
|
setLayoutResizer = (source, sClientSideKeyName, mode) =>
|
||||||
{
|
|
||||||
let event = 'fullscreenchange';
|
|
||||||
if (!el.requestFullscreen && el.webkitRequestFullscreen) {
|
|
||||||
el.requestFullscreen = el.webkitRequestFullscreen;
|
|
||||||
event = 'webkit'+event;
|
|
||||||
}
|
|
||||||
if (el.requestFullscreen) {
|
|
||||||
el.addEventListener(event, fn);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
|
||||||
{
|
{
|
||||||
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
||||||
target.removeAttribute('style');
|
|
||||||
source.removeAttribute('style');
|
source.removeAttribute('style');
|
||||||
}
|
}
|
||||||
|
source.observer?.disconnect();
|
||||||
// source.classList.toggle('resizable', mode);
|
// source.classList.toggle('resizable', mode);
|
||||||
if (mode) {
|
if (mode) {
|
||||||
const length = Local.get(sClientSideKeyName+mode);
|
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode);
|
||||||
|
if (length) {
|
||||||
|
source.style[mode.toLowerCase()] = length + 'px';
|
||||||
|
}
|
||||||
if (!source.layoutResizer) {
|
if (!source.layoutResizer) {
|
||||||
const resizer = createElement('div', {'class':'resizer'}),
|
const resizer = createElement('div', {'class':'resizer'}),
|
||||||
|
save = (data => Remote.saveSettings(0, data)).debounce(500),
|
||||||
size = {},
|
size = {},
|
||||||
store = () => {
|
store = () => {
|
||||||
if ('Width' == resizer.mode) {
|
const value = ('Width' == resizer.mode) ? source.offsetWidth : source.offsetHeight,
|
||||||
target.style.left = source.offsetWidth + 'px';
|
prop = resizer.key + resizer.mode;
|
||||||
Local.set(resizer.key+resizer.mode, source.offsetWidth);
|
(value == Local.get(prop)) || Local.set(prop, value);
|
||||||
} else {
|
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
|
||||||
target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
|
|
||||||
Local.set(resizer.key+resizer.mode, source.offsetHeight);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
cssint = s => {
|
cssint = s => {
|
||||||
let value = getComputedStyle(source, null)[s].replace('px', '');
|
let value = getComputedStyle(source, null)[s].replace('px', '');
|
||||||
|
|
@ -338,11 +285,90 @@ setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
||||||
}
|
}
|
||||||
source.layoutResizer.mode = mode;
|
source.layoutResizer.mode = mode;
|
||||||
source.layoutResizer.key = sClientSideKeyName;
|
source.layoutResizer.key = sClientSideKeyName;
|
||||||
source.observer && source.observer.observe(source, { box: 'border-box' });
|
source.observer?.observe(source, { box: 'border-box' });
|
||||||
if (length) {
|
}
|
||||||
source.style[mode] = length + 'px';
|
},
|
||||||
}
|
|
||||||
|
viewMessage = (oMessage, popup) => {
|
||||||
|
if (popup) {
|
||||||
|
oMessage.popupMessage();
|
||||||
} else {
|
} else {
|
||||||
source.observer && source.observer.disconnect();
|
MessageUserStore.error('');
|
||||||
|
let id = 'rl-msg-' + oMessage.hash,
|
||||||
|
body = oMessage.body || elementById(id);
|
||||||
|
if (!body) {
|
||||||
|
body = createElement('div',{
|
||||||
|
id:id,
|
||||||
|
hidden:'',
|
||||||
|
class:'b-text-part'
|
||||||
|
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||||
|
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||||
|
+ (oMessage.smimeSigned() ? ' smime-signed' : '')
|
||||||
|
+ (oMessage.smimeEncrypted() ? ' smime-encrypted' : '')
|
||||||
|
});
|
||||||
|
MessageUserStore.purgeCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
body.message = oMessage;
|
||||||
|
oMessage.body = body;
|
||||||
|
|
||||||
|
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
||||||
|
oMessage.viewPlain();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageUserStore.bodiesDom().append(body);
|
||||||
|
|
||||||
|
MessageUserStore.loading(false);
|
||||||
|
oMessage.body.hidden = false;
|
||||||
|
|
||||||
|
if (oMessage.isUnseen() && SettingsUserStore.messageReadAuto()) {
|
||||||
|
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||||
|
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||||
|
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
populateMessageBody = (oMessage, popup) => {
|
||||||
|
if (oMessage) {
|
||||||
|
popup || MessageUserStore.message(oMessage);
|
||||||
|
if (oMessage.body) {
|
||||||
|
viewMessage(oMessage, popup);
|
||||||
|
} else {
|
||||||
|
popup || MessageUserStore.loading(true);
|
||||||
|
Remote.message((iError, oData/*, bCached*/) => {
|
||||||
|
if (iError) {
|
||||||
|
if (Notifications.RequestAborted !== iError && !popup) {
|
||||||
|
MessageUserStore.message(null);
|
||||||
|
MessageUserStore.error(getNotification(iError));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let json = oData?.Result;
|
||||||
|
if (json
|
||||||
|
&& ((
|
||||||
|
oMessage.hash && oMessage.hash === json.hash
|
||||||
|
) || (
|
||||||
|
!oMessage.hash
|
||||||
|
&& oMessage.folder === json.folder
|
||||||
|
&& oMessage.uid == json.uid)
|
||||||
|
)
|
||||||
|
&& oMessage.revivePropertiesFromJson(json)
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
if (bCached) {
|
||||||
|
delete json.flags;
|
||||||
|
}
|
||||||
|
oMessage.body.remove();
|
||||||
|
*/
|
||||||
|
viewMessage(oMessage, popup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popup || MessageUserStore.loading(false);
|
||||||
|
}, oMessage.folder, oMessage.uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
leftPanelDisabled.subscribe(value => value && moveAction(0));
|
||||||
|
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
|
|
||||||
export class AbstractComponent {
|
|
||||||
constructor() {
|
|
||||||
this.disposable = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.disposable.forEach((funcToDispose) => {
|
|
||||||
if (funcToDispose && funcToDispose.dispose) {
|
|
||||||
funcToDispose.dispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import ko from 'ko';
|
|
||||||
import { AbstractComponent } from 'Component/Abstract';
|
|
||||||
|
|
||||||
class AbstractCheckbox extends AbstractComponent {
|
|
||||||
/**
|
|
||||||
* @param {Object} params = {}
|
|
||||||
*/
|
|
||||||
constructor(params = {}) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.value = ko.isObservable(params.value) ? params.value
|
|
||||||
: ko.observable(!!params.value);
|
|
||||||
|
|
||||||
this.enable = ko.isObservable(params.enable) ? params.enable
|
|
||||||
: ko.observable(undefined === params.enable || !!params.enable);
|
|
||||||
|
|
||||||
this.label = params.label || '';
|
|
||||||
this.inline = !!params.inline;
|
|
||||||
|
|
||||||
this.labeled = undefined !== params.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
click() {
|
|
||||||
this.enable() && this.value(!this.value());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AbstractCheckbox };
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import ko from 'ko';
|
|
||||||
import { pInt } from 'Common/Utils';
|
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
|
||||||
import { AbstractComponent } from 'Component/Abstract';
|
|
||||||
import { koComputable } from 'External/ko';
|
|
||||||
|
|
||||||
class AbstractInput extends AbstractComponent {
|
|
||||||
/**
|
|
||||||
* @param {Object} params
|
|
||||||
*/
|
|
||||||
constructor(params) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.value = params.value || '';
|
|
||||||
this.label = params.label || '';
|
|
||||||
this.enable = null == params.enable ? true : params.enable;
|
|
||||||
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null;
|
|
||||||
this.placeholder = params.placeholder || '';
|
|
||||||
|
|
||||||
this.labeled = null != params.label;
|
|
||||||
|
|
||||||
let size = 0 < params.size ? 'span' + params.size : '';
|
|
||||||
if (this.trigger) {
|
|
||||||
const
|
|
||||||
classForTrigger = ko.observable(''),
|
|
||||||
setTriggerState = value => {
|
|
||||||
switch (pInt(value)) {
|
|
||||||
case SaveSettingsStep.TrueResult:
|
|
||||||
classForTrigger('success');
|
|
||||||
break;
|
|
||||||
case SaveSettingsStep.FalseResult:
|
|
||||||
classForTrigger('error');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
classForTrigger('');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTriggerState(this.trigger());
|
|
||||||
|
|
||||||
this.className = koComputable(() =>
|
|
||||||
(size + ' settings-saved-trigger-input ' + classForTrigger()).trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.disposable.push(this.trigger.subscribe(setTriggerState, this));
|
|
||||||
} else {
|
|
||||||
this.className = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.disposable.push(this.className);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AbstractInput };
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
|
export class CheckboxComponent {
|
||||||
|
constructor(params = {}) {
|
||||||
|
this.name = params.name;
|
||||||
|
|
||||||
export class CheckboxComponent extends AbstractCheckbox {}
|
this.value = ko.isObservable(params.value) ? params.value
|
||||||
|
: ko.observable(!!params.value);
|
||||||
|
|
||||||
|
this.enable = ko.isObservable(params.enable) ? params.enable
|
||||||
|
: ko.observable(params.enable ?? 1);
|
||||||
|
|
||||||
|
this.label = params.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
click() {
|
||||||
|
this.enable() && this.value(!this.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,30 @@
|
||||||
import { doc, createElement } from 'Common/Globals';
|
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||||
import { isArray } from 'Common/Utils';
|
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
|
import { addressparser } from 'Mime/Address';
|
||||||
|
|
||||||
const contentType = 'snappymail/emailaddress',
|
const contentType = 'snappymail/emailaddress',
|
||||||
getAddressKey = li => li && li.emailaddress && li.emailaddress.key;
|
getAddressKey = li => li?.emailaddress?.key,
|
||||||
|
|
||||||
|
parseEmailLine = line => addressparser(line).map(item =>
|
||||||
|
(item.name || item.email)
|
||||||
|
? new EmailModel(item.email, item.name) : null
|
||||||
|
).filter(v => v),
|
||||||
|
splitEmailLine = line => {
|
||||||
|
const result = [];
|
||||||
|
let exists = false;
|
||||||
|
addressparser(line).forEach(item => {
|
||||||
|
const address = (item.name || item.email)
|
||||||
|
? new EmailModel(item.email, item.name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (address?.email) {
|
||||||
|
exists = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(address ? address.toLine() : item.name);
|
||||||
|
});
|
||||||
|
return exists ? result : null;
|
||||||
|
};
|
||||||
|
|
||||||
let dragAddress, datalist;
|
let dragAddress, datalist;
|
||||||
|
|
||||||
|
|
@ -17,10 +38,12 @@ export class EmailAddressesComponent {
|
||||||
doc.body.append(datalist);
|
doc.body.append(datalist);
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this,
|
const self = this,
|
||||||
|
input = createElement('input',{type:"text", list:datalist.id,
|
||||||
|
autocomplete:"off", autocorrect:"off", autocapitalize:"off"}),
|
||||||
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
|
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
|
||||||
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
|
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
|
||||||
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
|
validDropzone = () => dragAddress?.li.parentNode !== self.ul,
|
||||||
fnDrag = e => validDropzone() && e.preventDefault();
|
fnDrag = e => validDropzone() && e.preventDefault();
|
||||||
|
|
||||||
self.element = element;
|
self.element = element;
|
||||||
|
|
@ -42,57 +65,61 @@ export class EmailAddressesComponent {
|
||||||
// Create the elements
|
// Create the elements
|
||||||
self.ul = createElement('ul',{class:"emailaddresses"});
|
self.ul = createElement('ul',{class:"emailaddresses"});
|
||||||
|
|
||||||
self.ul.addEventListener('click', e => self._focus(e));
|
addEventsListeners(self.ul, {
|
||||||
self.ul.addEventListener('dblclick', e => self._editTag(e));
|
click: e => self._focus(e),
|
||||||
self.ul.addEventListener("dragenter", fnDrag);
|
dblclick: e => self._editTag(e),
|
||||||
self.ul.addEventListener("dragover", fnDrag);
|
dragenter: fnDrag,
|
||||||
self.ul.addEventListener("drop", e => {
|
dragover: fnDrag,
|
||||||
if (validDropzone() && dragAddress.value) {
|
drop: e => {
|
||||||
e.preventDefault();
|
if (validDropzone() && dragAddress.value) {
|
||||||
dragAddress.source._removeDraggedTag(dragAddress.li);
|
|
||||||
self._parseValue(dragAddress.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.input = createElement('input',{type:"text", list:datalist.id,
|
|
||||||
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
|
|
||||||
|
|
||||||
self.input.addEventListener('focus', () => self._focusTrigger(true));
|
|
||||||
self.input.addEventListener('blur', () => {
|
|
||||||
// prevent autoComplete menu click from causing a false 'blur'
|
|
||||||
self._parseInput(true);
|
|
||||||
self._focusTrigger(false);
|
|
||||||
});
|
|
||||||
self.input.addEventListener('keydown', e => {
|
|
||||||
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
|
|
||||||
// if our input contains no value and backspace has been pressed, select the last tag
|
|
||||||
var lastTag = self.inputCont.previousElementSibling,
|
|
||||||
input = self.input;
|
|
||||||
if (lastTag && (!input.value
|
|
||||||
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
lastTag.querySelector('a').focus();
|
dragAddress.source._removeDraggedTag(dragAddress.li);
|
||||||
|
self._parseValue(dragAddress.value);
|
||||||
}
|
}
|
||||||
self._updateDatalist();
|
|
||||||
} else if (e.key == 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
self._parseInput(true);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.input.addEventListener('input', () => {
|
|
||||||
self._parseInput();
|
self.input = input;
|
||||||
self._updateDatalist();
|
|
||||||
|
addEventsListeners(input, {
|
||||||
|
focus: () => {
|
||||||
|
self._focusTrigger(true);
|
||||||
|
input.value || self._resetDatalist();
|
||||||
|
},
|
||||||
|
blur: () => {
|
||||||
|
// prevent autoComplete menu click from causing a false 'blur'
|
||||||
|
self._parseInput(true);
|
||||||
|
self._focusTrigger(false);
|
||||||
|
},
|
||||||
|
keydown: e => {
|
||||||
|
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
|
||||||
|
// if our input contains no value and backspace has been pressed, select the last tag
|
||||||
|
var lastTag = self.inputCont.previousElementSibling;
|
||||||
|
if (lastTag && (!input.value
|
||||||
|
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
lastTag.querySelector('a').focus();
|
||||||
|
}
|
||||||
|
self._updateDatalist();
|
||||||
|
} else if (e.key == 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
self._parseInput(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: () => {
|
||||||
|
self._parseInput();
|
||||||
|
self._updateDatalist();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
self.input.addEventListener('focus', () => self.input.value || self._resetDatalist());
|
|
||||||
|
|
||||||
// define starting placeholder
|
// define starting placeholder
|
||||||
if (element.placeholder) {
|
if (element.placeholder) {
|
||||||
self.input.placeholder = element.placeholder;
|
input.placeholder = element.placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.inputCont = createElement('li',{class:"emailaddresses-input"});
|
self.inputCont = createElement('li',{class:"emailaddresses-input"});
|
||||||
self.inputCont.append(self.input);
|
self.inputCont.append(input);
|
||||||
self.ul.append(self.inputCont);
|
self.ul.append(self.inputCont);
|
||||||
|
|
||||||
element.replaceWith(self.ul);
|
element.replaceWith(self.ul);
|
||||||
|
|
@ -104,14 +131,23 @@ export class EmailAddressesComponent {
|
||||||
|
|
||||||
self._updateDatalist = self.options.autoCompleteSource
|
self._updateDatalist = self.options.autoCompleteSource
|
||||||
? (() => {
|
? (() => {
|
||||||
let value = self.input.value.trim();
|
let value = input.value.trim();
|
||||||
if (datalist.inputValue !== value) {
|
if (datalist.inputValue !== value) {
|
||||||
datalist.inputValue = value;
|
datalist.inputValue = value;
|
||||||
value.length && self.options.autoCompleteSource(
|
value.length && self.options.autoCompleteSource(
|
||||||
{term:value},
|
value,
|
||||||
items => {
|
items => {
|
||||||
self._resetDatalist();
|
self._resetDatalist();
|
||||||
items && items.forEach(item => datalist.append(new Option(item)));
|
let chars = value.length;
|
||||||
|
items?.forEach(item => {
|
||||||
|
datalist.append(new Option(item));
|
||||||
|
chars = Math.max(chars, item.length);
|
||||||
|
});
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/368 and #513
|
||||||
|
chars *= 8;
|
||||||
|
if (input.clientWidth < chars) {
|
||||||
|
input.style.width = chars + 'px';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -130,28 +166,66 @@ export class EmailAddressesComponent {
|
||||||
|
|
||||||
_parseInput(force) {
|
_parseInput(force) {
|
||||||
let val = this.input.value;
|
let val = this.input.value;
|
||||||
if (force || val.includes(',') || val.includes(';')) {
|
if ((force || val.includes(',') || val.includes(';')
|
||||||
this._parseValue(val) && (this.input.value = '');
|
|| (val.charAt(val.length-1)===' ' && this._simpleEmailMatch(val)))
|
||||||
|
&& this._parseValue(val)) {
|
||||||
|
this.input.value = '';
|
||||||
}
|
}
|
||||||
this._resizeInput();
|
this._resizeInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseValue(val) {
|
_parseValue(val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
var self = this,
|
const self = this,
|
||||||
values = [];
|
v = val.trim(),
|
||||||
|
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
|
||||||
const v = val.trim(),
|
values = (hook || [val]).map(value => parseEmailLine(value))
|
||||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1)))
|
.flat(Infinity)
|
||||||
? EmailModel.splitEmailLine(val)
|
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
|
||||||
: null;
|
|
||||||
|
|
||||||
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
|
||||||
.flat(Infinity)
|
|
||||||
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
|
|
||||||
|
|
||||||
if (values.length) {
|
if (values.length) {
|
||||||
self._setChosen(values);
|
values.forEach(a => {
|
||||||
|
var v = a[0].trim(),
|
||||||
|
exists = false,
|
||||||
|
lastIndex = -1,
|
||||||
|
obj = {
|
||||||
|
key : '',
|
||||||
|
obj : null,
|
||||||
|
value : ''
|
||||||
|
};
|
||||||
|
|
||||||
|
self._chosenValues.forEach((vv, kk) => {
|
||||||
|
if (vv.value === self._lastEdit) {
|
||||||
|
lastIndex = kk;
|
||||||
|
}
|
||||||
|
|
||||||
|
exists |= vv.value === v;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (v !== '' && a[1] && !exists) {
|
||||||
|
|
||||||
|
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
|
||||||
|
obj.value = v;
|
||||||
|
obj.obj = a[1];
|
||||||
|
|
||||||
|
if (-1 < lastIndex) {
|
||||||
|
self._chosenValues.splice(lastIndex, 0, obj);
|
||||||
|
} else {
|
||||||
|
self._chosenValues.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
self._lastEdit = '';
|
||||||
|
self._renderTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (1 === values.length && '' === values[0] && '' !== self._lastEdit) {
|
||||||
|
self._lastEdit = '';
|
||||||
|
self._renderTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
self._setValue(self._buildValue());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +235,7 @@ export class EmailAddressesComponent {
|
||||||
_resizeInput() {
|
_resizeInput() {
|
||||||
let input = this.input;
|
let input = this.input;
|
||||||
if (input.clientWidth < input.scrollWidth) {
|
if (input.clientWidth < input.scrollWidth) {
|
||||||
input.style.width = Math.min(500, Math.max(200, input.scrollWidth)) + 'px';
|
input.style.width = (input.scrollWidth + 20) + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,56 +276,6 @@ export class EmailAddressesComponent {
|
||||||
self._resizeInput(ev);
|
self._resizeInput(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setChosen(valArr) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (!isArray(valArr)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
valArr.forEach(a => {
|
|
||||||
var v = a[0].trim(),
|
|
||||||
exists = false,
|
|
||||||
lastIndex = -1,
|
|
||||||
obj = {
|
|
||||||
key : '',
|
|
||||||
obj : null,
|
|
||||||
value : ''
|
|
||||||
};
|
|
||||||
|
|
||||||
self._chosenValues.forEach((vv, kk) => {
|
|
||||||
if (vv.value === self._lastEdit) {
|
|
||||||
lastIndex = kk;
|
|
||||||
}
|
|
||||||
|
|
||||||
vv.value === v && (exists = true);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (v !== '' && a && a[1] && !exists) {
|
|
||||||
|
|
||||||
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
|
|
||||||
obj.value = v;
|
|
||||||
obj.obj = a[1];
|
|
||||||
|
|
||||||
if (-1 < lastIndex) {
|
|
||||||
self._chosenValues.splice(lastIndex, 0, obj);
|
|
||||||
} else {
|
|
||||||
self._chosenValues.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
self._lastEdit = '';
|
|
||||||
self._renderTags();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (valArr.length === 1 && valArr[0] === '' && self._lastEdit !== '') {
|
|
||||||
self._lastEdit = '';
|
|
||||||
self._renderTags();
|
|
||||||
}
|
|
||||||
|
|
||||||
self._setValue(self._buildValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildValue() {
|
_buildValue() {
|
||||||
return this._chosenValues.map(v => v.value).join(',');
|
return this._chosenValues.map(v => v.value).join(',');
|
||||||
}
|
}
|
||||||
|
|
@ -263,79 +287,90 @@ export class EmailAddressesComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_simpleEmailMatch(value) {
|
||||||
|
// A very SIMPLE test to check if the value might be an email
|
||||||
|
const val = value.trim();
|
||||||
|
return /^[^@]*<[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}>$/g.test(val)
|
||||||
|
|| /^[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}$/g.test(val);
|
||||||
|
}
|
||||||
|
|
||||||
_renderTags() {
|
_renderTags() {
|
||||||
let self = this;
|
let self = this;
|
||||||
[...self.ul.children].forEach(node => node !== self.inputCont && node.remove());
|
[...self.ul.children].forEach(node => node !== self.inputCont && node.remove());
|
||||||
|
|
||||||
self._chosenValues.forEach(v => {
|
self._chosenValues.forEach(v => {
|
||||||
if (v.obj) {
|
if (v.obj) {
|
||||||
let li = createElement('li',{title:v.obj.toLine(false, false, true),draggable:'true'}),
|
let li = createElement('li',{title:v.obj.toLine(),draggable:'true'}),
|
||||||
el = createElement('span');
|
el = createElement('span');
|
||||||
el.append(v.obj.toLine(true, false, true));
|
el.append(v.obj.toLine(true));
|
||||||
li.append(el);
|
li.append(el);
|
||||||
|
|
||||||
el = createElement('a',{href:'#', class:'ficon'});
|
el = createElement('a',{href:'#', class:'ficon'});
|
||||||
el.append('✖');
|
el.append('✖');
|
||||||
el.addEventListener('click', e => self._removeTag(e, li));
|
addEventsListeners(el, {
|
||||||
el.addEventListener('focus', () => li.className = 'emailaddresses-selected');
|
click: e => self._removeTag(e, li),
|
||||||
el.addEventListener('blur', () => li.className = null);
|
focus: () => li.className = 'emailaddresses-selected',
|
||||||
el.addEventListener('keydown', e => {
|
blur: () => li.className = null,
|
||||||
switch (e.key) {
|
keydown: e => {
|
||||||
case 'Delete':
|
switch (e.key) {
|
||||||
case 'Backspace':
|
case 'Delete':
|
||||||
self._removeTag(e, li);
|
case 'Backspace':
|
||||||
break;
|
self._removeTag(e, li);
|
||||||
|
break;
|
||||||
|
|
||||||
// 'e' - edit tag (removes tag and places value into visible input
|
// 'e' - edit tag (removes tag and places value into visible input
|
||||||
case 'e':
|
case 'e':
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
self._editTag(e);
|
self._editTag(e);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
// select the previous tag or input if no more tags exist
|
// select the previous tag or input if no more tags exist
|
||||||
var previous = el.closest('li').previousElementSibling;
|
var previous = el.closest('li').previousElementSibling;
|
||||||
if (previous.matches('li')) {
|
if (previous.matches('li')) {
|
||||||
previous.querySelector('a').focus();
|
previous.querySelector('a').focus();
|
||||||
} else {
|
} else {
|
||||||
self.focus();
|
self.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
// select the next tag or input if no more tags exist
|
// select the next tag or input if no more tags exist
|
||||||
var next = el.closest('li').nextElementSibling;
|
var next = el.closest('li').nextElementSibling;
|
||||||
if (next !== this.inputCont) {
|
if (next !== this.inputCont) {
|
||||||
next.querySelector('a').focus();
|
next.querySelector('a').focus();
|
||||||
} else {
|
} else {
|
||||||
this.focus();
|
this.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
self._focus(e);
|
self._focus(e);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
li.append(el);
|
li.append(el);
|
||||||
|
|
||||||
li.emailaddress = v;
|
li.emailaddress = v;
|
||||||
|
|
||||||
li.addEventListener("dragstart", e => {
|
addEventsListeners(li, {
|
||||||
dragAddress = {
|
dragstart: e => {
|
||||||
source: self,
|
dragAddress = {
|
||||||
li: li,
|
source: self,
|
||||||
value: li.emailaddress.obj.toLine()
|
li: li,
|
||||||
};
|
value: li.emailaddress.obj.toLine()
|
||||||
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
|
};
|
||||||
e.dataTransfer.setData('text/plain', contentType);
|
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
|
||||||
// e.dataTransfer.setDragImage(li, 0, 0);
|
e.dataTransfer.setData('text/plain', contentType);
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
// e.dataTransfer.setDragImage(li, 0, 0);
|
||||||
li.style.opacity = 0.25;
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
});
|
li.style.opacity = 0.25;
|
||||||
li.addEventListener("dragend", () => {
|
},
|
||||||
dragAddress = null;
|
dragend: () => {
|
||||||
li.style.cssText = '';
|
dragAddress = null;
|
||||||
|
li.style.cssText = '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.inputCont.before(li);
|
self.inputCont.before(li);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import { AbstractInput } from 'Component/AbstractInput';
|
|
||||||
|
|
||||||
export class InputComponent extends AbstractInput {}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
|
|
||||||
|
|
||||||
export class CheckboxMaterialDesignComponent extends AbstractCheckbox {}
|
|
||||||
|
|
@ -1,20 +1,56 @@
|
||||||
import { i18n } from 'Common/Translator';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
|
import { dispose, koComputable } from 'External/ko';
|
||||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
import { AbstractInput } from 'Component/AbstractInput';
|
|
||||||
|
|
||||||
export class SelectComponent extends AbstractInput {
|
export class SelectComponent {
|
||||||
/**
|
/**
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
*/
|
*/
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
super(params);
|
this.value = params.value;
|
||||||
|
this.label = params.label;
|
||||||
|
this.trigger = params.trigger?.subscribe ? params.trigger : null;
|
||||||
|
this.placeholder = params.placeholder;
|
||||||
|
this.options = params.options;
|
||||||
|
this.optionsText = params.optionsText;
|
||||||
|
this.optionsValue = params.optionsValue;
|
||||||
|
|
||||||
this.options = params.options || '';
|
let size = 0 < params.size ? 'span' + params.size : '';
|
||||||
|
if (this.trigger) {
|
||||||
|
const
|
||||||
|
classForTrigger = ko.observable(''),
|
||||||
|
setTriggerState = value => {
|
||||||
|
switch (value) {
|
||||||
|
case SaveSettingStatus.Success:
|
||||||
|
classForTrigger('success');
|
||||||
|
break;
|
||||||
|
case SaveSettingStatus.Failed:
|
||||||
|
classForTrigger('error');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
classForTrigger('');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.optionsText = params.optionsText || null;
|
setTriggerState(this.trigger());
|
||||||
this.optionsValue = params.optionsValue || null;
|
|
||||||
this.optionsCaption = i18n(params.optionsCaption || null);
|
this.className = koComputable(() =>
|
||||||
|
(size + ' settings-save-trigger-input ' + classForTrigger()).trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.disposables = [
|
||||||
|
this.trigger.subscribe(setTriggerState, this),
|
||||||
|
this.className
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
this.className = size;
|
||||||
|
}
|
||||||
|
|
||||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.disposables?.forEach(dispose);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { AbstractInput } from 'Component/AbstractInput';
|
|
||||||
|
|
||||||
export class TextAreaComponent extends AbstractInput {
|
|
||||||
/**
|
|
||||||
* @param {Object} params
|
|
||||||
*/
|
|
||||||
constructor(params) {
|
|
||||||
super(params);
|
|
||||||
|
|
||||||
this.rows = params.rows || 5;
|
|
||||||
this.spellcheck = !!params.spellcheck;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
227
dev/DAV/JCard.js
Normal file
227
dev/DAV/JCard.js
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
/**
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc7095
|
||||||
|
*
|
||||||
|
* Inspired by https://github.com/mcpar-land/vcfer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { VCardProperty } from './VCardProperty'
|
||||||
|
|
||||||
|
export class JCard {
|
||||||
|
|
||||||
|
constructor(input)
|
||||||
|
{
|
||||||
|
this.props = new Map()
|
||||||
|
this.version = '4.0'
|
||||||
|
if (input) {
|
||||||
|
// read from jCard
|
||||||
|
if (typeof input !== 'object') {
|
||||||
|
throw Error('error reading vcard')
|
||||||
|
}
|
||||||
|
this.parseFromJCard(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFromJCard(json)
|
||||||
|
{
|
||||||
|
json = JSON.parse(JSON.stringify(json));
|
||||||
|
if (!/vcard/i.test(json[0])) {
|
||||||
|
throw new SyntaxError('Incorrect jCard format');
|
||||||
|
}
|
||||||
|
json[1].forEach(jprop => this.add(new VCardProperty(jprop)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an array of {@link VCardProperty} objects under the specified field.
|
||||||
|
* Returns [] if there are no VCardProperty objects found.
|
||||||
|
* Properites are always stored in an array.
|
||||||
|
* @param field to get.
|
||||||
|
* @param type If provided, only return {@link VCardProperty}s with the specified
|
||||||
|
* type as a param.
|
||||||
|
*/
|
||||||
|
get(field, type)
|
||||||
|
{
|
||||||
|
if (type) {
|
||||||
|
let props = this.props.get(field);
|
||||||
|
return props
|
||||||
|
? props.filter(prop => {
|
||||||
|
let types = prop.type;
|
||||||
|
return (Array.isArray(types) ? types : [types]).includes(type);
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
return this.props.get(field) || [];
|
||||||
|
// TODO with type filter-er
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a _single_ VCardProperty of the specified field. Attempts to pick based
|
||||||
|
* on the following priorities, in order:
|
||||||
|
* - `TYPE={type}` of the value specified in the `type` argument. Ignored
|
||||||
|
* if the argument isn't supplied.
|
||||||
|
* - `TYPE=pref` is present.
|
||||||
|
* - is the VCardProperty at index 0 from get(field)
|
||||||
|
* @param field
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
getOne(field, type)
|
||||||
|
{
|
||||||
|
return this.get(field, type || 'pref')[0] || this.get(field)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the contents of a field to contain a single {@link VCardProperty}.
|
||||||
|
*
|
||||||
|
* Accepts either 2-4 arguments to construct a VCardProperty,
|
||||||
|
* or 1 argument of a preexisting VCardProperty object.
|
||||||
|
*
|
||||||
|
* This will always overwrite all existing properties of the given
|
||||||
|
* field. For just adding a new VCardProperty, see {@link VCard#add}
|
||||||
|
* @param arg the field, or a VCardProperty object
|
||||||
|
* @param value the value for the VCardProperty object
|
||||||
|
* @param params the parameters for the VCardProperty object
|
||||||
|
* @param type the type for the VCardProperty object
|
||||||
|
*/
|
||||||
|
set(arg, value, params, type)
|
||||||
|
{
|
||||||
|
if (typeof arg === 'string') {
|
||||||
|
arg = new VCardProperty(String(arg), value, params, type);
|
||||||
|
}
|
||||||
|
if (!(arg instanceof VCardProperty)) {
|
||||||
|
throw Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
||||||
|
}
|
||||||
|
let field = arg.getField();
|
||||||
|
this.props.set(field, [arg]);
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(arg, value, params, type)
|
||||||
|
{
|
||||||
|
// string arguments
|
||||||
|
if (typeof arg === 'string') {
|
||||||
|
arg = new VCardProperty(String(arg), value, params, type);
|
||||||
|
}
|
||||||
|
if (!(arg instanceof VCardProperty)) {
|
||||||
|
throw Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
||||||
|
}
|
||||||
|
// VCardProperty arguments
|
||||||
|
let field = arg.getField();
|
||||||
|
if (this.props.get(field)) this.props.get(field)?.push(arg)
|
||||||
|
else this.props.set(field, [arg])
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a {@link VCardProperty}, or all properties of the supplied field.
|
||||||
|
* @param arg the field, or a {@link VCardProperty} object
|
||||||
|
* @param paramFilter (incomplete)
|
||||||
|
*/
|
||||||
|
remove(arg) {
|
||||||
|
// string arguments
|
||||||
|
if (typeof arg === 'string') {
|
||||||
|
// TODO filter by param
|
||||||
|
this.props.delete(arg);
|
||||||
|
}
|
||||||
|
// VCardProperty argument
|
||||||
|
else if (arg instanceof VCardProperty) {
|
||||||
|
let propArray = this.props.get(arg.getField());
|
||||||
|
if (!propArray?.includes(arg))
|
||||||
|
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||||
|
propArray.splice(propArray.indexOf(arg), 1);
|
||||||
|
if (propArray.length === 0)
|
||||||
|
this.props.delete(arg.getField());
|
||||||
|
}
|
||||||
|
// incorrect arguments
|
||||||
|
else
|
||||||
|
throw Error('invalid argument of VCard.remove(), expects ' +
|
||||||
|
'string and optional param filter or a VCardProperty');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the vCard has at least one @{link VCardProperty}
|
||||||
|
* of the given field.
|
||||||
|
* @param field The field to query
|
||||||
|
*/
|
||||||
|
has(field)
|
||||||
|
{
|
||||||
|
return (!!this.props.get(field) && this.props.get(field).length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns stringified JSON
|
||||||
|
*/
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link JCard} object as a JSON array.
|
||||||
|
*/
|
||||||
|
toJSON()
|
||||||
|
{
|
||||||
|
let data = [['version', {}, 'text', '4.0']];
|
||||||
|
/*
|
||||||
|
this.props.forEach((props, field) =>
|
||||||
|
(field === 'version') || props.forEach(prop => prop.isEmpty() || data.push(prop.toJSON()))
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
for (const [field, props] of this.props.entries()) {
|
||||||
|
if ('version' !== field) {
|
||||||
|
for (const prop of props) {
|
||||||
|
prop.isEmpty() || data.push(prop.toJSON());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['vcard', data];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically generate the 'fn' VCardProperty from the preferred 'n' VCardProperty.
|
||||||
|
*
|
||||||
|
* #### `set` (`boolean`)
|
||||||
|
*
|
||||||
|
* - `false`: (default) return the generated full name string without
|
||||||
|
* modifying the VCard.
|
||||||
|
*
|
||||||
|
* - `true`: modify the VCard's `fn` VCardProperty directly, as specified
|
||||||
|
* by `append`
|
||||||
|
*
|
||||||
|
* #### `append` (`boolean`)
|
||||||
|
*
|
||||||
|
* (ignored if `set` is `false`)
|
||||||
|
*
|
||||||
|
* - `false`: (default) replace the existing 'fn' VCardProperty/properties with
|
||||||
|
* a new one.
|
||||||
|
*
|
||||||
|
* - `true`: append a new `fn` VCardProperty to the array.
|
||||||
|
*
|
||||||
|
* see: [RFC 6350 section 6.2.1](https://tools.ietf.org/html/rfc6350#section-6.2.1)
|
||||||
|
*/
|
||||||
|
parseFullName(options) {
|
||||||
|
let n = this.getOne('n');
|
||||||
|
if (n === undefined) {
|
||||||
|
throw Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
||||||
|
}
|
||||||
|
let fnString = '';
|
||||||
|
// Position in n -> position in fn
|
||||||
|
[3, 1, 2, 0, 4].forEach(pos => {
|
||||||
|
let splitStr = n.value[pos];
|
||||||
|
if (splitStr) {
|
||||||
|
// comma separated values separated by spaces
|
||||||
|
fnString += ' ' + splitStr.replace(',', ' ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fnString = fnString.trim();
|
||||||
|
let fn = new VCardProperty('fn', fnString);
|
||||||
|
if (options?.set) {
|
||||||
|
if (options.append) {
|
||||||
|
this.add(fn);
|
||||||
|
} else {
|
||||||
|
this.set(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
146
dev/DAV/VCardProperty.js
Normal file
146
dev/DAV/VCardProperty.js
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
/**
|
||||||
|
* Inspired by https://github.com/mcpar-land/vcfer
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class VCardProperty {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class describing a single vCard property.
|
||||||
|
* Will almost always be a member of a
|
||||||
|
* {@link VCard}'s [props]{@link VCard.props} map.
|
||||||
|
*
|
||||||
|
* Accepts either 2-4 arguments, or 1 argument in jCard property format.
|
||||||
|
* @param arg the field, or a jCard property
|
||||||
|
* @param value
|
||||||
|
* @param params
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
constructor(arg, value, params, type = 'text')
|
||||||
|
{
|
||||||
|
this.field = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the value of the property.
|
||||||
|
* @example '(123) 456 7890'
|
||||||
|
*/
|
||||||
|
this.value = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the type of the property value.
|
||||||
|
* @example 'text'
|
||||||
|
*/
|
||||||
|
this.type = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5
|
||||||
|
* An jCard parameters object.
|
||||||
|
* @example
|
||||||
|
* {
|
||||||
|
* type: ['work', 'voice', 'pref'],
|
||||||
|
* value: 'uri'
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
this.params = {};
|
||||||
|
|
||||||
|
// Construct from arguments
|
||||||
|
if (value !== undefined && typeof arg === 'string') {
|
||||||
|
this.field = arg;
|
||||||
|
this.value = value;
|
||||||
|
this.params = params || {};
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
// construct from jcard
|
||||||
|
else if (value === undefined && params === undefined && typeof arg === 'object') {
|
||||||
|
this.parseFromJCardProperty(arg);
|
||||||
|
}
|
||||||
|
// invalid property
|
||||||
|
else {
|
||||||
|
throw Error('invalid Property constructor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFromJCardProperty(jCardProp)
|
||||||
|
{
|
||||||
|
jCardProp = JSON.parse(JSON.stringify(jCardProp));
|
||||||
|
this.field = jCardProp[0].toLowerCase();
|
||||||
|
this.params = jCardProp[1];
|
||||||
|
this.type = jCardProp[2];
|
||||||
|
this.value = jCardProp[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
addParam(key, value)
|
||||||
|
{
|
||||||
|
if (Array.isArray(this.params[key])) {
|
||||||
|
this.params[key].push(value);
|
||||||
|
}
|
||||||
|
else if (this.params[key] != null) {
|
||||||
|
this.params[key] = [this.params[key], value];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.params[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a copy of the Property's string value. */
|
||||||
|
getValue()
|
||||||
|
{
|
||||||
|
return '' + this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.3
|
||||||
|
*/
|
||||||
|
pref()
|
||||||
|
{
|
||||||
|
return this.params.pref || 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.6
|
||||||
|
*/
|
||||||
|
tags()
|
||||||
|
{
|
||||||
|
return this.params.type || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if all the following are true:
|
||||||
|
* - the property's value contains charactes other than `;`
|
||||||
|
* - the property has no parameters
|
||||||
|
*/
|
||||||
|
isEmpty()
|
||||||
|
{
|
||||||
|
return ((null == this.value || !/[^;]+/.test(this.value)) && !Object.keys(this.params).length);
|
||||||
|
}
|
||||||
|
|
||||||
|
notEmpty()
|
||||||
|
{
|
||||||
|
return !this.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a readonly string copy of the property's field. */
|
||||||
|
getField()
|
||||||
|
{
|
||||||
|
return '' + this.field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns stringified JSON
|
||||||
|
*/
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a JSON array in a [jCard property]{@link JCardProperty} format
|
||||||
|
*/
|
||||||
|
toJSON()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
this.field,
|
||||||
|
this.params,
|
||||||
|
this.type || 'text',
|
||||||
|
this.value
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
494
dev/External/SquireUI.js
vendored
494
dev/External/SquireUI.js
vendored
|
|
@ -3,10 +3,6 @@
|
||||||
(doc => {
|
(doc => {
|
||||||
|
|
||||||
const
|
const
|
||||||
removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
|
|
||||||
allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,FONT,H1,H2,H3,H4,H5,H6,HR,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
|
|
||||||
allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
|
|
||||||
|
|
||||||
i18n = (str, def) => rl.i18n(str) || def,
|
i18n = (str, def) => rl.i18n(str) || def,
|
||||||
|
|
||||||
ctrlKey = shortcuts.getMetaKey() + ' + ',
|
ctrlKey = shortcuts.getMetaKey() + ' + ',
|
||||||
|
|
@ -14,74 +10,55 @@ const
|
||||||
createElement = name => doc.createElement(name),
|
createElement = name => doc.createElement(name),
|
||||||
|
|
||||||
tpl = createElement('template'),
|
tpl = createElement('template'),
|
||||||
clr = createElement('input'),
|
|
||||||
|
|
||||||
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
|
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
|
||||||
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
|
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
|
||||||
plainToHtml = text => rl.Utils.plainToHtml(text),
|
plainToHtml = text => rl.Utils.plainToHtml(text),
|
||||||
|
|
||||||
getFragmentOfChildren = parent => {
|
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||||
let frag = doc.createDocumentFragment();
|
|
||||||
frag.append(...parent.childNodes);
|
|
||||||
return frag;
|
|
||||||
},
|
|
||||||
|
|
||||||
SquireDefaultConfig = {
|
SquireDefaultConfig = {
|
||||||
/*
|
/*
|
||||||
blockTag: 'P',
|
|
||||||
undo: {
|
|
||||||
documentSizeThreshold: -1, // -1 means no threshold
|
|
||||||
undoLimit: -1 // -1 means no limit
|
|
||||||
},
|
|
||||||
addLinks: true // allow_smart_html_links
|
addLinks: true // allow_smart_html_links
|
||||||
*/
|
*/
|
||||||
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
||||||
tpl.innerHTML = (html||'')
|
html = (html||'')
|
||||||
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
|
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
|
||||||
.replace(/<!--[^>]+-->/g,'')
|
.replace(/<!--[^>]+-->/g,'')
|
||||||
.replace(/<span[^>]*>\s*<\/span>/gi,'')
|
.replace(/<span[^>]*>\s*<\/span>/gi,'')
|
||||||
.trim();
|
.trim();
|
||||||
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
tpl.innerHTML = isPaste ? rl.Utils.cleanHtml(html).html : html;
|
||||||
tpl.querySelectorAll('[data-x-div-type]').forEach(el => el.replaceWith(getFragmentOfChildren(el)));
|
|
||||||
if (isPaste) {
|
|
||||||
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
|
|
||||||
tpl.querySelectorAll('*').forEach(el => {
|
|
||||||
if (!el.matches(allowedElements)) {
|
|
||||||
el.replaceWith(getFragmentOfChildren(el));
|
|
||||||
} else if (el.hasAttributes()) {
|
|
||||||
[...el.attributes].forEach(attr => {
|
|
||||||
let name = attr.name.toLowerCase();
|
|
||||||
if (!allowedAttributes.includes(name)) {
|
|
||||||
el.removeAttribute(name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return tpl.content;
|
return tpl.content;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
clr.type = "color";
|
|
||||||
clr.style.display = 'none';
|
|
||||||
doc.body.append(clr);
|
|
||||||
|
|
||||||
class SquireUI
|
class SquireUI
|
||||||
{
|
{
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
const
|
const
|
||||||
doClr = fn => () => {
|
clr = createElement('input'),
|
||||||
|
doClr = name => input => {
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/826
|
||||||
|
clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px';
|
||||||
|
clr.style.width = input.offsetWidth + 'px';
|
||||||
|
|
||||||
clr.value = '';
|
clr.value = '';
|
||||||
clr.onchange = () => squire[fn](clr.value);
|
clr.onchange = () => squire.setStyle({[name]:clr.value});
|
||||||
clr.click();
|
// Chrome 110+ https://github.com/the-djmaze/snappymail/issues/1199
|
||||||
|
// clr.oninput = () => squire.setStyle({[name]:clr.value});
|
||||||
|
setTimeout(()=>clr.click(),1);
|
||||||
},
|
},
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
mode: {
|
mode: {
|
||||||
plain: {
|
plain: {
|
||||||
html: '〈〉',
|
// html: '〈〉',
|
||||||
cmd: () => this.setMode('plain' == this.mode ? 'wysiwyg' : 'plain'),
|
// cmd: () => this.setMode('plain' == this.mode ? 'wysiwyg' : 'plain'),
|
||||||
hint: i18n('EDITOR/TEXT_SWITCHER_PLAINT_TEXT', 'Plain')
|
select: [
|
||||||
|
[i18n('SETTINGS_GENERAL/EDITOR_HTML'),'wysiwyg'],
|
||||||
|
[i18n('SETTINGS_GENERAL/EDITOR_PLAIN'),'plain']
|
||||||
|
],
|
||||||
|
cmd: s => this.setMode('plain' == s.value ? 'plain' : 'wysiwyg')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
|
|
@ -107,72 +84,83 @@ class SquireUI
|
||||||
cmd: s => squire.setStyle({ fontFamily: s.value })
|
cmd: s => squire.setStyle({ fontFamily: s.value })
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
select: ['11px','13px','16px','20px','24px','30px'],
|
select: [[i18n('GLOBAL/DEFAULT'),''],'11px','13px','16px','20px','24px','30px'],
|
||||||
|
defaultValueIndex: 0,
|
||||||
cmd: s => squire.setStyle({ fontSize: s.value })
|
cmd: s => squire.setStyle({ fontSize: s.value })
|
||||||
|
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
|
||||||
|
// example:
|
||||||
|
// select: ['','xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
|
||||||
|
// defaultValueIndex: 0,
|
||||||
|
},
|
||||||
|
// dir: {
|
||||||
|
// select: [
|
||||||
|
// [i18n('EDITOR/DIR_LTR', 'LTR'),'ltr'],
|
||||||
|
// [i18n('EDITOR/DIR_RTL', 'RTL'),'rtl'],
|
||||||
|
// [i18n('EDITOR/DIR_AUTO', 'Auto'),'auto'],
|
||||||
|
// ['',''],
|
||||||
|
// ],
|
||||||
|
// cmd: s => {
|
||||||
|
// squire.setAttribute('dir', s.value || null);
|
||||||
|
// // squire.setStyle({ 'unicode-bidi': 'plaintext' });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
dir: {
|
||||||
|
dir_ltr: {
|
||||||
|
html: '⁋',
|
||||||
|
cmd: () => squire.setTextDirection('ltr')
|
||||||
|
},
|
||||||
|
dir_rtl: {
|
||||||
|
html: '¶',
|
||||||
|
cmd: () => squire.setTextDirection('rtl')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
textColor: {
|
textColor: {
|
||||||
html: 'A<sub>▾</sub>',
|
html: 'A<sub>▾</sub>',
|
||||||
cmd: doClr('setTextColor'),
|
cmd: doClr('color')
|
||||||
hint: 'Text color'
|
|
||||||
},
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
html: '🎨', /* ▧ */
|
html: '🎨', /* ▧ */
|
||||||
cmd: doClr('setBackgroundColor'),
|
cmd: doClr('backgroundColor')
|
||||||
hint: 'Background color'
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/*
|
|
||||||
bidi: {
|
|
||||||
bdoLtr: {
|
|
||||||
html: '‎𝐁',
|
|
||||||
cmd: () => this.doAction('bold','B'),
|
|
||||||
hint: 'Bold'
|
|
||||||
},
|
|
||||||
bdoRtl: {
|
|
||||||
html: '‏𝐁',
|
|
||||||
cmd: () => this.doAction('bold','B'),
|
|
||||||
hint: 'Bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
inline: {
|
inline: {
|
||||||
bold: {
|
bold: {
|
||||||
html: 'B',
|
html: 'B',
|
||||||
cmd: () => this.doAction('bold'),
|
cmd: () => this.doAction('bold'),
|
||||||
key: 'B',
|
key: 'B',
|
||||||
hint: 'Bold'
|
matches: 'B,STRONG'
|
||||||
},
|
},
|
||||||
italic: {
|
italic: {
|
||||||
html: 'I',
|
html: 'I',
|
||||||
cmd: () => this.doAction('italic'),
|
cmd: () => this.doAction('italic'),
|
||||||
key: 'I',
|
key: 'I',
|
||||||
hint: 'Italic'
|
matches: 'I'
|
||||||
},
|
},
|
||||||
underline: {
|
underline: {
|
||||||
html: '<u>U</u>',
|
html: '<u>U</u>',
|
||||||
cmd: () => this.doAction('underline'),
|
cmd: () => this.doAction('underline'),
|
||||||
key: 'U',
|
key: 'U',
|
||||||
hint: 'Underline'
|
matches: 'U'
|
||||||
},
|
},
|
||||||
strike: {
|
strike: {
|
||||||
html: '<s>S</s>',
|
html: '<s>S</s>',
|
||||||
cmd: () => this.doAction('strikethrough'),
|
cmd: () => this.doAction('strikethrough'),
|
||||||
key: 'Shift + 7',
|
key: 'Shift + 7',
|
||||||
hint: 'Strikethrough'
|
matches: 'S'
|
||||||
},
|
},
|
||||||
sub: {
|
sub: {
|
||||||
html: 'Xₙ',
|
html: 'Xₙ',
|
||||||
cmd: () => this.doAction('subscript'),
|
cmd: () => this.doAction('subscript'),
|
||||||
key: 'Shift + 5',
|
key: 'Shift + 5',
|
||||||
hint: 'Subscript'
|
matches: 'SUB'
|
||||||
},
|
},
|
||||||
sup: {
|
sup: {
|
||||||
html: 'Xⁿ',
|
html: 'Xⁿ',
|
||||||
cmd: () => this.doAction('superscript'),
|
cmd: () => this.doAction('superscript'),
|
||||||
key: 'Shift + 6',
|
key: 'Shift + 6',
|
||||||
hint: 'Superscript'
|
matches: 'SUP'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
block: {
|
block: {
|
||||||
|
|
@ -180,64 +168,58 @@ class SquireUI
|
||||||
html: '#',
|
html: '#',
|
||||||
cmd: () => this.doList('OL'),
|
cmd: () => this.doList('OL'),
|
||||||
key: 'Shift + 8',
|
key: 'Shift + 8',
|
||||||
hint: 'Ordered list'
|
matches: 'OL'
|
||||||
},
|
},
|
||||||
ul: {
|
ul: {
|
||||||
html: '⋮',
|
html: '⋮',
|
||||||
cmd: () => this.doList('UL'),
|
cmd: () => this.doList('UL'),
|
||||||
key: 'Shift + 9',
|
key: 'Shift + 9',
|
||||||
hint: 'Unordered list'
|
matches: 'UL'
|
||||||
},
|
},
|
||||||
quote: {
|
quote: {
|
||||||
html: '"',
|
html: '"',
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
let parent = this.getParentNodeName('UL,OL');
|
let parent = squire.getSelectionClosest('UL,OL,BLOCKQUOTE')?.nodeName;
|
||||||
(parent && 'BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel();
|
('BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel();
|
||||||
},
|
},
|
||||||
hint: 'Blockquote'
|
matches: 'BLOCKQUOTE'
|
||||||
},
|
},
|
||||||
indentDecrease: {
|
indentDecrease: {
|
||||||
html: '⇤',
|
html: '⇤',
|
||||||
cmd: () => squire.changeIndentationLevel('decrease'),
|
cmd: () => squire.changeIndentationLevel('decrease'),
|
||||||
key: ']',
|
key: ']'
|
||||||
hint: 'Decrease indent'
|
|
||||||
},
|
},
|
||||||
indentIncrease: {
|
indentIncrease: {
|
||||||
html: '⇥',
|
html: '⇥',
|
||||||
cmd: () => squire.changeIndentationLevel('increase'),
|
cmd: () => squire.changeIndentationLevel('increase'),
|
||||||
key: '[',
|
key: '['
|
||||||
hint: 'Increase indent'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
targets: {
|
targets: {
|
||||||
link: {
|
link: {
|
||||||
html: '🔗',
|
html: '🔗',
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
if ('A' === this.getParentNodeName()) {
|
let node = squire.getSelectionClosest('A'),
|
||||||
squire.removeLink();
|
url = prompt("Link", node?.href || "https://");
|
||||||
} else {
|
if (url != null) {
|
||||||
let url = prompt("Link","https://");
|
url.length ? squire.makeLink(url) : (node && squire.removeLink());
|
||||||
url != null && url.length && squire.makeLink(url);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hint: 'Link'
|
matches: 'A'
|
||||||
},
|
},
|
||||||
imageUrl: {
|
imageUrl: {
|
||||||
html: '🖼️',
|
html: '🖼️',
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
if ('IMG' === this.getParentNodeName()) {
|
let node = squire.getSelectionClosest('IMG'),
|
||||||
// squire.removeLink();
|
src = prompt("Image", node?.src || "https://");
|
||||||
} else {
|
src?.length ? squire.insertImage(src) : node?.remove();
|
||||||
let src = prompt("Image","https://");
|
|
||||||
src != null && src.length && squire.insertImage(src);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
hint: 'Image URL'
|
matches: 'IMG'
|
||||||
},
|
},
|
||||||
imageUpload: {
|
imageUpload: {
|
||||||
html: '📂️',
|
html: '📂️',
|
||||||
cmd: () => browseImage.click(),
|
cmd: () => browseImage.click(),
|
||||||
hint: 'Image select',
|
matches: 'IMG'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
|
|
@ -249,14 +231,26 @@ class SquireUI
|
||||||
undo: {
|
undo: {
|
||||||
html: '↶',
|
html: '↶',
|
||||||
cmd: () => squire.undo(),
|
cmd: () => squire.undo(),
|
||||||
key: 'Z',
|
key: 'Z'
|
||||||
hint: 'Undo'
|
|
||||||
},
|
},
|
||||||
redo: {
|
redo: {
|
||||||
html: '↷',
|
html: '↷',
|
||||||
cmd: () => squire.redo(),
|
cmd: () => squire.redo(),
|
||||||
key: 'Y',
|
key: 'Y'
|
||||||
hint: 'Redo'
|
},
|
||||||
|
source: {
|
||||||
|
html: '👁',
|
||||||
|
cmd: btn => {
|
||||||
|
this.setMode('source' == this.mode ? 'wysiwyg' : 'source');
|
||||||
|
btn.classList.toggle('active', 'source' == this.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: {
|
||||||
|
removeStyle: {
|
||||||
|
html: '⎚',
|
||||||
|
cmd: () => squire.setStyle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -267,6 +261,30 @@ class SquireUI
|
||||||
browseImage = createElement('input'),
|
browseImage = createElement('input'),
|
||||||
squire = new Squire(wysiwyg, SquireDefaultConfig);
|
squire = new Squire(wysiwyg, SquireDefaultConfig);
|
||||||
|
|
||||||
|
clr.type = 'color';
|
||||||
|
toolbar.append(clr);
|
||||||
|
// Chrome https://github.com/the-djmaze/snappymail/issues/1199
|
||||||
|
let clrid = 'squire-colors',
|
||||||
|
colorlist = doc.getElementById(clrid),
|
||||||
|
add = hex => colorlist.append(new Option(hex));
|
||||||
|
if (!colorlist) {
|
||||||
|
colorlist = createElement('datalist');
|
||||||
|
colorlist.id = clrid;
|
||||||
|
// Color blind safe Tableau 10 by Maureen Stone
|
||||||
|
add('#4E79A7');
|
||||||
|
add('#F28E2B');
|
||||||
|
add('#E15759');
|
||||||
|
add('#76B7B2');
|
||||||
|
add('#59A14F');
|
||||||
|
add('#EDC948');
|
||||||
|
add('#B07AA1');
|
||||||
|
add('#FF9DA7');
|
||||||
|
add('#9C755F');
|
||||||
|
add('#BAB0AC');
|
||||||
|
doc.body.append(colorlist);
|
||||||
|
}
|
||||||
|
clr.setAttribute('list', clrid);
|
||||||
|
|
||||||
browseImage.type = 'file';
|
browseImage.type = 'file';
|
||||||
browseImage.accept = 'image/*';
|
browseImage.accept = 'image/*';
|
||||||
browseImage.style.display = 'none';
|
browseImage.style.display = 'none';
|
||||||
|
|
@ -280,25 +298,18 @@ class SquireUI
|
||||||
|
|
||||||
plain.className = 'squire-plain';
|
plain.className = 'squire-plain';
|
||||||
wysiwyg.className = 'squire-wysiwyg';
|
wysiwyg.className = 'squire-wysiwyg';
|
||||||
|
wysiwyg.dir = 'auto';
|
||||||
this.mode = ''; // 'plain' | 'wysiwyg'
|
this.mode = ''; // 'plain' | 'wysiwyg'
|
||||||
this.__plain = {
|
|
||||||
getRawData: () => this.plain.value,
|
|
||||||
setRawData: plain => this.plain.value = plain
|
|
||||||
};
|
|
||||||
|
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.squire = squire;
|
this.squire = squire;
|
||||||
this.plain = plain;
|
this.plain = plain;
|
||||||
this.wysiwyg = wysiwyg;
|
this.wysiwyg = wysiwyg;
|
||||||
|
|
||||||
|
dispatchEvent(new CustomEvent('squire-toolbar', {detail:{squire:this,actions:actions}}));
|
||||||
|
|
||||||
toolbar.className = 'squire-toolbar btn-toolbar';
|
toolbar.className = 'squire-toolbar btn-toolbar';
|
||||||
let group, action, touchTap;
|
let group, action/*, touchTap*/;
|
||||||
for (group in actions) {
|
for (group in actions) {
|
||||||
/*
|
|
||||||
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
let toolgroup = createElement('div');
|
let toolgroup = createElement('div');
|
||||||
toolgroup.className = 'btn-group';
|
toolgroup.className = 'btn-group';
|
||||||
toolgroup.id = 'squire-toolgroup-'+group;
|
toolgroup.id = 'squire-toolgroup-'+group;
|
||||||
|
|
@ -313,11 +324,13 @@ class SquireUI
|
||||||
input.className = 'btn';
|
input.className = 'btn';
|
||||||
if (Array.isArray(cfg.select)) {
|
if (Array.isArray(cfg.select)) {
|
||||||
cfg.select.forEach(value => {
|
cfg.select.forEach(value => {
|
||||||
var option = new Option(value, value);
|
value = Array.isArray(value) ? value : [value, value];
|
||||||
option.style[action] = value;
|
var option = new Option(value[0], value[1]);
|
||||||
|
option.style[action] = value[1];
|
||||||
input.append(option);
|
input.append(option);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
input.add(new Option(i18n('GLOBAL/DEFAULT'), ''));
|
||||||
Object.entries(cfg.select).forEach(([label, options]) => {
|
Object.entries(cfg.select).forEach(([label, options]) => {
|
||||||
let group = createElement('optgroup');
|
let group = createElement('optgroup');
|
||||||
group.label = label;
|
group.label = label;
|
||||||
|
|
@ -326,7 +339,7 @@ class SquireUI
|
||||||
option.style[action] = value;
|
option.style[action] = value;
|
||||||
group.append(option);
|
group.append(option);
|
||||||
});
|
});
|
||||||
input.append(group);
|
input.add(group);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ev = 'input';
|
ev = 'input';
|
||||||
|
|
@ -336,18 +349,21 @@ class SquireUI
|
||||||
input.className = 'btn';
|
input.className = 'btn';
|
||||||
input.innerHTML = cfg.html;
|
input.innerHTML = cfg.html;
|
||||||
input.action_cmd = cfg.cmd;
|
input.action_cmd = cfg.cmd;
|
||||||
input.addEventListener('touchstart', () => touchTap = input, {passive:true});
|
/*
|
||||||
input.addEventListener('touchmove', () => touchTap = null, {passive:true});
|
input.addEventListener('pointerdown', () => touchTap = input, {passive:true});
|
||||||
input.addEventListener('touchcancel', () => touchTap = null);
|
input.addEventListener('pointermove', () => touchTap = null, {passive:true});
|
||||||
input.addEventListener('touchend', e => {
|
input.addEventListener('pointercancel', () => touchTap = null);
|
||||||
|
input.addEventListener('pointerup', e => {
|
||||||
if (touchTap === input) {
|
if (touchTap === input) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
cfg.cmd(input);
|
cfg.cmd(input);
|
||||||
}
|
}
|
||||||
touchTap = null;
|
touchTap = null;
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
input.addEventListener(ev, () => cfg.cmd(input));
|
input.addEventListener(ev, () => cfg.cmd(input));
|
||||||
|
cfg.hint = i18n('EDITOR/' + action.toUpperCase());
|
||||||
if (cfg.hint) {
|
if (cfg.hint) {
|
||||||
input.title = cfg.key ? cfg.hint + ' (' + ctrlKey + cfg.key + ')' : cfg.hint;
|
input.title = cfg.key ? cfg.hint + ' (' + ctrlKey + cfg.key + ')' : cfg.hint;
|
||||||
} else if (cfg.key) {
|
} else if (cfg.key) {
|
||||||
|
|
@ -361,38 +377,182 @@ class SquireUI
|
||||||
toolgroup.children.length && toolbar.append(toolgroup);
|
toolgroup.children.length && toolbar.append(toolgroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.modeSelect = actions.mode.plain.input;
|
||||||
|
|
||||||
let changes = actions.changes;
|
let changes = actions.changes;
|
||||||
changes.undo.input.disabled = changes.redo.input.disabled = true;
|
changes.undo.input.disabled = changes.redo.input.disabled = true;
|
||||||
squire.addEventListener('undoStateChange', state => {
|
squire.addEventListener('undoStateChange', e => {
|
||||||
changes.undo.input.disabled = !state.canUndo;
|
changes.undo.input.disabled = !e.detail.canUndo;
|
||||||
changes.redo.input.disabled = !state.canRedo;
|
changes.redo.input.disabled = !e.detail.canRedo;
|
||||||
});
|
});
|
||||||
|
|
||||||
squire.addEventListener('focus', () => shortcuts.off());
|
squire.addEventListener('pasteImage', e => {
|
||||||
squire.addEventListener('blur', () => shortcuts.on());
|
const items = e.detail.clipboardData.items;
|
||||||
|
let l = items.length;
|
||||||
|
while (l--) {
|
||||||
|
const item = items[l];
|
||||||
|
if (/^image\/(png|jpeg|webp)/.test(item.type)) {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.onload = event => {
|
||||||
|
let img = createElement("img"),
|
||||||
|
canvas = createElement("canvas"),
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
img.onload = ()=>{
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
let width = img.width, height = img.height;
|
||||||
|
if (width > height) {
|
||||||
|
// Landscape
|
||||||
|
if (width > 1024) {
|
||||||
|
height = height * 1024 / width;
|
||||||
|
width = 1024;
|
||||||
|
}
|
||||||
|
} else if (height > 1024) {
|
||||||
|
// Portrait
|
||||||
|
width = width * 1024 / height;
|
||||||
|
height = 1024;
|
||||||
|
}
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
squire.insertHTML('<img alt="" style="width:100%;max-width:'+width+'px" src="'+canvas.toDataURL()+'">', true);
|
||||||
|
};
|
||||||
|
img.src = event.target.result;
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(item.getAsFile());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||||
|
|
||||||
|
// squire.addEventListener('focus', () => shortcuts.off());
|
||||||
|
// squire.addEventListener('blur', () => shortcuts.on());
|
||||||
|
|
||||||
container.append(toolbar, wysiwyg, plain);
|
container.append(toolbar, wysiwyg, plain);
|
||||||
|
|
||||||
/*
|
/**
|
||||||
squire.addEventListener('dragover', );
|
* @param {string} fontName
|
||||||
squire.addEventListener('drop', );
|
* @return {string}
|
||||||
squire.addEventListener('pathChange', );
|
*/
|
||||||
squire.addEventListener('cursor', );
|
const normalizeFontName = (fontName) => fontName.trim().replace(/(^["']*|["']*$)/g, '').trim().toLowerCase();
|
||||||
squire.addEventListener('select', );
|
|
||||||
squire.addEventListener('input', );
|
|
||||||
squire.addEventListener('willPaste', );
|
|
||||||
squire.addEventListener( 'keydown keyup', monitorShiftKey )
|
|
||||||
squire.addEventListener( 'keydown', onKey )
|
|
||||||
*/
|
|
||||||
|
|
||||||
// CKEditor gimmicks used by HtmlEditor
|
/** @type {string[]} - lower cased array of available font families*/
|
||||||
this.plugins = {
|
const fontFamiliesLowerCase = Object.values(actions.font.fontFamily.input.options).map(option => option.value.toLowerCase());
|
||||||
plain: true
|
|
||||||
};
|
/**
|
||||||
this.focusManager = {
|
* A theme might have CSS like div.squire-wysiwyg[contenteditable="true"] {
|
||||||
hasFocus: () => squire._isFocused,
|
* font-family: 'Times New Roman', Times, serif; }
|
||||||
blur: () => squire.blur()
|
* so let's find the best match squire.getRoot()'s font
|
||||||
|
* it will also help to properly handle generic font names like 'sans-serif'
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
let defaultFontFamilyIndex = 0;
|
||||||
|
const squireRootFonts = getComputedStyle(squire.getRoot()).fontFamily.split(',').map(normalizeFontName);
|
||||||
|
fontFamiliesLowerCase.some((family, index) => {
|
||||||
|
const matchFound = family.split(',').some(availableFontName => {
|
||||||
|
const normalizedFontName = normalizeFontName(availableFontName);
|
||||||
|
return squireRootFonts.some(squireFontName => squireFontName === normalizedFontName);
|
||||||
|
});
|
||||||
|
if (matchFound) {
|
||||||
|
defaultFontFamilyIndex = index;
|
||||||
|
}
|
||||||
|
return matchFound;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instead of comparing whole 'font-family' strings,
|
||||||
|
* we are going to look for individual font names, because we might be
|
||||||
|
* editing a Draft started in another email client for example
|
||||||
|
*
|
||||||
|
* @type {Object.<string,number>}
|
||||||
|
*/
|
||||||
|
const fontNamesMap = {};
|
||||||
|
/**
|
||||||
|
* @param {string} fontFamily
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
const processFontFamilyString = (fontFamily, index) => {
|
||||||
|
fontFamily.split(',').forEach(fontName => {
|
||||||
|
const key = normalizeFontName(fontName);
|
||||||
|
if (fontNamesMap[key] === undefined) {
|
||||||
|
fontNamesMap[key] = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
// first deal with the default font family
|
||||||
|
processFontFamilyString(fontFamiliesLowerCase[defaultFontFamilyIndex], defaultFontFamilyIndex);
|
||||||
|
// and now with the rest of the font families
|
||||||
|
fontFamiliesLowerCase.forEach((fontFamily, index) => {
|
||||||
|
if (index !== defaultFontFamilyIndex) {
|
||||||
|
processFontFamilyString(fontFamily, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----
|
||||||
|
|
||||||
|
squire.addEventListener('pathChange', () => {
|
||||||
|
|
||||||
|
const squireRoot = squire.getRoot();
|
||||||
|
let range = squire.getSelection(),
|
||||||
|
collapsed = range.collapsed,
|
||||||
|
elm = collapsed ? range.endContainer : range?.commonAncestorContainer;
|
||||||
|
if (elm && !(elm instanceof Element)) {
|
||||||
|
elm = elm.parentElement;
|
||||||
|
}
|
||||||
|
forEachObjectValue(actions, entries => {
|
||||||
|
forEachObjectValue(entries, cfg => {
|
||||||
|
// Check if selection has a matching parent or contains a matching element
|
||||||
|
cfg.matches && cfg.input.classList.toggle('active', !!(elm && (
|
||||||
|
(!collapsed && [...elm.querySelectorAll(cfg.matches)].some(node => range.intersectsNode(node)))
|
||||||
|
|| elm.closestWithin(cfg.matches, squireRoot)
|
||||||
|
)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (elm) {
|
||||||
|
// try to find font-family and/or font-size and set "select" elements' values
|
||||||
|
|
||||||
|
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||||
|
let familySelectedIndex = defaultFontFamilyIndex;
|
||||||
|
|
||||||
|
let familyFound = false;
|
||||||
|
let sizeFound = false;
|
||||||
|
do {
|
||||||
|
if (!familyFound && elm.style.fontFamily) {
|
||||||
|
familyFound = true;
|
||||||
|
familySelectedIndex = -1; // show empty select if we don't know the font
|
||||||
|
const fontNames = elm.style.fontFamily.split(',');
|
||||||
|
for (let i = 0; i < fontNames.length; i++) {
|
||||||
|
const index = fontNamesMap[normalizeFontName(fontNames[i])];
|
||||||
|
if (index !== undefined) {
|
||||||
|
familySelectedIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sizeFound && elm.style.fontSize) {
|
||||||
|
sizeFound = true;
|
||||||
|
// -1 is ok because it will just show a black <select>
|
||||||
|
sizeSelectedIndex = actions.font.fontSize.select.indexOf(elm.style.fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
elm = elm.parentElement;
|
||||||
|
} while ((!familyFound || !sizeFound) && elm && elm !== squireRoot);
|
||||||
|
|
||||||
|
actions.font.fontFamily.input.selectedIndex = familySelectedIndex;
|
||||||
|
actions.font.fontSize.input.selectedIndex = sizeSelectedIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
squire.addEventListener('cursor', e => {
|
||||||
|
console.dir({cursor:e.detail.range});
|
||||||
|
});
|
||||||
|
squire.addEventListener('select', e => {
|
||||||
|
console.dir({select:e.detail.range});
|
||||||
|
});
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
doAction(name) {
|
doAction(name) {
|
||||||
|
|
@ -400,39 +560,36 @@ class SquireUI
|
||||||
this.squire.focus();
|
this.squire.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentNodeName(selector) {
|
|
||||||
let parent = this.squire.getSelectionClosest(selector);
|
|
||||||
return parent ? parent.nodeName : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
doList(type) {
|
doList(type) {
|
||||||
let parent = this.getParentNodeName('UL,OL'),
|
let parent = this.squire.getSelectionClosest('UL,OL')?.nodeName,
|
||||||
fn = {UL:'makeUnorderedList',OL:'makeOrderedList'};
|
fn = {UL:'makeUnorderedList',OL:'makeOrderedList'};
|
||||||
(parent && parent == type) ? this.squire.removeList() : this.squire[fn[type]]();
|
(parent == type) ? this.squire.removeList() : this.squire[fn[type]]();
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
testPresenceinSelection(format, validation) {
|
testPresenceinSelection(format, validation) {
|
||||||
return validation.test(this.squire.getPath()) || this.squire.hasFormat(format);
|
return validation.test(this.squire.getPath()) || this.squire.hasFormat(format);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
setMode(mode) {
|
setMode(mode) {
|
||||||
if (this.mode != mode) {
|
if (this.mode != mode) {
|
||||||
let cl = this.container.classList;
|
let cl = this.container.classList, source = 'source' == this.mode;
|
||||||
cl.remove('squire-mode-'+this.mode);
|
cl.remove('squire-mode-'+this.mode);
|
||||||
if ('plain' == mode) {
|
if ('plain' == mode) {
|
||||||
this.plain.value = htmlToPlain(this.squire.getHTML(), true);
|
this.plain.value = htmlToPlain(source ? this.plain.value : this.squire.getHTML(), true);
|
||||||
|
} else if ('source' == mode) {
|
||||||
|
this.plain.value = this.squire.getHTML();
|
||||||
} else {
|
} else {
|
||||||
this.setData(plainToHtml(this.plain.value, true));
|
this.setData(source ? this.plain.value : plainToHtml(this.plain.value, true));
|
||||||
mode = 'wysiwyg';
|
mode = 'wysiwyg';
|
||||||
}
|
}
|
||||||
this.mode = mode; // 'wysiwyg' or 'plain'
|
this.mode = mode; // 'wysiwyg' or 'plain'
|
||||||
cl.add('squire-mode-'+mode);
|
cl.add('squire-mode-'+mode);
|
||||||
this.onModeChange && this.onModeChange();
|
this.onModeChange?.();
|
||||||
setTimeout(()=>this.focus(),1);
|
setTimeout(()=>this.focus(),1);
|
||||||
}
|
}
|
||||||
|
this.modeSelect.selectedIndex = 'plain' == this.mode ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CKeditor gimmicks used by HtmlEditor
|
|
||||||
on(type, fn) {
|
on(type, fn) {
|
||||||
if ('mode' == type) {
|
if ('mode' == type) {
|
||||||
this.onModeChange = fn;
|
this.onModeChange = fn;
|
||||||
|
|
@ -464,12 +621,18 @@ class SquireUI
|
||||||
}
|
}
|
||||||
this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature;
|
this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature;
|
||||||
} else {
|
} else {
|
||||||
const root = this.squire.getRoot(),
|
const squire = this.squire,
|
||||||
|
root = squire.getRoot(),
|
||||||
|
br = createElement('br'),
|
||||||
div = createElement('div');
|
div = createElement('div');
|
||||||
div.className = 'rl-signature';
|
div.className = 'rl-signature';
|
||||||
div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature);
|
div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature);
|
||||||
root.querySelectorAll('div.rl-signature').forEach(node => node.remove());
|
root.querySelectorAll('div.rl-signature').forEach(node => node.remove());
|
||||||
cfg.insertBefore ? root.prepend(div) : root.append(div);
|
cfg.insertBefore ? root.prepend(div) : root.append(div);
|
||||||
|
// Move cursor above signature
|
||||||
|
div.before(br);
|
||||||
|
div.before(br.cloneNode());
|
||||||
|
// squire._docWasChanged();
|
||||||
}
|
}
|
||||||
this._prev_txt_sig = signature;
|
this._prev_txt_sig = signature;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -479,16 +642,39 @@ class SquireUI
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() {
|
getData() {
|
||||||
return trimLines(this.squire.getHTML());
|
return 'source' == this.mode ? this.plain.value : trimLines(this.squire.getHTML());
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(html) {
|
setData(html) {
|
||||||
// this.plain.value = html;
|
// this.plain.value = html;
|
||||||
this.squire.setHTML(trimLines(html));
|
const squire = this.squire;
|
||||||
|
squire.setHTML(trimLines(html));
|
||||||
|
const node = squire.getRoot(),
|
||||||
|
range = squire.getSelection();
|
||||||
|
range.setStart(node, 0);
|
||||||
|
range.setEnd(node, 0);
|
||||||
|
squire.setSelection( range );
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlainData() {
|
||||||
|
return this.plain.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlainData(text) {
|
||||||
|
this.plain.value = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
this.squire.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
('plain' == this.mode ? this.plain : this.squire).focus();
|
if ('plain' == this.mode) {
|
||||||
|
this.plain.focus();
|
||||||
|
this.plain.setSelectionRange(0, 0);
|
||||||
|
} else {
|
||||||
|
this.squire.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
399
dev/External/User/ko.js
vendored
399
dev/External/User/ko.js
vendored
|
|
@ -1,17 +1,22 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { HtmlEditor } from 'Common/Html';
|
import { RFC822 } from 'Common/File';
|
||||||
import { timeToNode } from 'Common/Momentor';
|
import { HtmlEditor } from 'Common/HtmlEditor';
|
||||||
import { elementById } from 'Common/Globals';
|
import { timeToNode } from 'Common/Translator';
|
||||||
import { isArray } from 'Common/Utils';
|
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
import { dropFilesInFolder } from 'Common/Folders';
|
||||||
|
import { setExpandedFolder } from 'Model/FolderCollection';
|
||||||
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
||||||
const rlContentType = 'snappymail/action',
|
const rlContentType = 'snappymail/action',
|
||||||
|
|
||||||
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
|
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
|
||||||
// In Chrome Mobile dataTransfer.types.includes(rlContentType) fails, only text/plain is set
|
// In Chrome Mobile dataTransfer.types.includes(rlContentType) fails, only text/plain is set
|
||||||
getDragAction = () => dragData ? dragData.action : false,
|
dragMessages = () => 'messages' === dragData?.action,
|
||||||
|
dragSortable = () => 'sortable' === dragData?.action,
|
||||||
setDragAction = (e, action, effect, data, img) => {
|
setDragAction = (e, action, effect, data, img) => {
|
||||||
dragData = {
|
dragData = {
|
||||||
action: action,
|
action: action,
|
||||||
|
|
@ -24,229 +29,219 @@ const rlContentType = 'snappymail/action',
|
||||||
},
|
},
|
||||||
|
|
||||||
dragTimer = {
|
dragTimer = {
|
||||||
id: 0,
|
id: 0
|
||||||
stop: () => clearTimeout(dragTimer.id),
|
},
|
||||||
start: fn => dragTimer.id = setTimeout(fn, 500)
|
|
||||||
};
|
dragStop = (e, element) => {
|
||||||
|
e.preventDefault();
|
||||||
|
element?.classList.remove('droppableHover');
|
||||||
|
if (dragTimer.node == element) {
|
||||||
|
dragTimer.node = null;
|
||||||
|
clearTimeout(dragTimer.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dragEnter = (e, element, folder) => {
|
||||||
|
let files = false;
|
||||||
|
// if (e.dataTransfer.types.includes('Files'))
|
||||||
|
for (const item of e.dataTransfer.items) {
|
||||||
|
files |= 'file' === item.kind && RFC822 === item.type;
|
||||||
|
}
|
||||||
|
if (files || dragMessages()) {
|
||||||
|
e.stopPropagation();
|
||||||
|
dragStop(e, dragTimer.node);
|
||||||
|
e.dataTransfer.dropEffect = files ? 'copy' : (e.ctrlKey ? 'copy' : 'move');
|
||||||
|
element.classList.add('droppableHover');
|
||||||
|
if (folder.collapsed()) {
|
||||||
|
dragTimer.node = element;
|
||||||
|
dragTimer.id = setTimeout(() => {
|
||||||
|
folder.collapsed(false);
|
||||||
|
setExpandedFolder(folder.fullName, true);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dragDrop = (e, element, folder, dragData) => {
|
||||||
|
dragStop(e, element);
|
||||||
|
if (dragMessages() && 'copyMove' == e.dataTransfer.effectAllowed) {
|
||||||
|
MessagelistUserStore.moveMessages(
|
||||||
|
FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey
|
||||||
|
);
|
||||||
|
} else if (e.dataTransfer.types.includes('Files')) {
|
||||||
|
dropFilesInFolder(folder.fullName, e.dataTransfer.files);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
|
||||||
|
|
||||||
let dragImage,
|
let dragImage,
|
||||||
dragData;
|
dragData;
|
||||||
|
|
||||||
ko.bindingHandlers.editor = {
|
Object.assign(ko.bindingHandlers, {
|
||||||
init: (element, fValueAccessor) => {
|
|
||||||
let editor = null;
|
|
||||||
|
|
||||||
const fValue = fValueAccessor(),
|
editor: {
|
||||||
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
|
init: (element, fValueAccessor) => {
|
||||||
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
let editor = null;
|
||||||
fOnReady = () => {
|
|
||||||
fValue.__editor = editor;
|
|
||||||
fUpdateEditorValue();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ko.isObservable(fValue) && HtmlEditor) {
|
const fValue = fValueAccessor(),
|
||||||
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
|
fUpdateEditorValue = () => fValue.__editor?.setHtmlOrPlain(fValue()),
|
||||||
|
fUpdateKoValue = () => fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
||||||
|
fOnReady = () => {
|
||||||
|
fValue.__editor = editor;
|
||||||
|
fUpdateEditorValue();
|
||||||
|
};
|
||||||
|
|
||||||
fValue.__fetchEditorValue = fUpdateKoValue;
|
if (ko.isObservable(fValue)) {
|
||||||
|
editor = new HtmlEditor(element, fOnReady, fUpdateKoValue, fUpdateKoValue);
|
||||||
|
|
||||||
fValue.subscribe(fUpdateEditorValue);
|
fValue.__fetchEditorValue = fUpdateKoValue;
|
||||||
|
|
||||||
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
|
fValue.subscribe(fUpdateEditorValue);
|
||||||
// });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
|
// ko.addDisposeCallback(element, () => {
|
||||||
ko.bindingHandlers.moment = {
|
// });
|
||||||
init: ttn,
|
}
|
||||||
update: ttn
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.emailsTags = {
|
|
||||||
init: (element, fValueAccessor, fAllBindings) => {
|
|
||||||
const fValue = fValueAccessor();
|
|
||||||
|
|
||||||
element.addresses = new EmailAddressesComponent(element, {
|
|
||||||
focusCallback: value => fValue.focused && fValue.focused(!!value),
|
|
||||||
autoCompleteSource: fAllBindings.get('autoCompleteSource'),
|
|
||||||
onChange: value => fValue(value)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fValue.focused && fValue.focused.subscribe) {
|
|
||||||
fValue.focused.subscribe(value =>
|
|
||||||
element.addresses[value ? 'focus' : 'blur']()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
element.addresses.value = ko.unwrap(fValueAccessor());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start dragging selected messages
|
time: {
|
||||||
ko.bindingHandlers.dragmessages = {
|
init: ttn,
|
||||||
init: (element, fValueAccessor) => {
|
update: ttn
|
||||||
element.addEventListener("dragstart", e => {
|
},
|
||||||
let data = fValueAccessor()(e);
|
|
||||||
dragImage || (dragImage = elementById('messagesDragImage'));
|
|
||||||
if (data && dragImage && !ThemeStore.isMobile()) {
|
|
||||||
dragImage.querySelector('.text').textContent = data.uids.length;
|
|
||||||
let img = dragImage.querySelector('i');
|
|
||||||
img.classList.toggle('icon-copy', e.ctrlKey);
|
|
||||||
img.classList.toggle('icon-mail', !e.ctrlKey);
|
|
||||||
|
|
||||||
// Else Chrome doesn't show it
|
emailsTags: {
|
||||||
dragImage.style.left = e.clientX + 'px';
|
init: (element, fValueAccessor, fAllBindings) => {
|
||||||
dragImage.style.top = e.clientY + 'px';
|
const fValue = fValueAccessor(),
|
||||||
dragImage.style.right = 'auto';
|
focused = fValue.focused;
|
||||||
|
|
||||||
setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
|
element.addresses = new EmailAddressesComponent(element, {
|
||||||
|
focusCallback: value => focused?.(!!value),
|
||||||
|
autoCompleteSource: fAllBindings.get('autoCompleteSource'),
|
||||||
|
onChange: value => fValue(value)
|
||||||
|
});
|
||||||
|
|
||||||
// Remove the Chrome visibility
|
focused?.subscribe(value =>
|
||||||
dragImage.style.cssText = '';
|
element.addresses[value ? 'focus' : 'blur']()
|
||||||
} else {
|
);
|
||||||
e.preventDefault();
|
},
|
||||||
}
|
update: (element, fValueAccessor) => {
|
||||||
|
element.addresses.value = ko.unwrap(fValueAccessor());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
}, false);
|
// Start dragging checked messages
|
||||||
element.addEventListener("dragend", () => dragData = null);
|
dragmessages: {
|
||||||
element.setAttribute('draggable', true);
|
init: element => {
|
||||||
}
|
element.addEventListener("dragstart", e => {
|
||||||
};
|
dragImage || (dragImage = elementById('messagesDragImage'));
|
||||||
|
if (dragImage && !ThemeStore.isMobile()) {
|
||||||
|
ko.dataFor(doc.elementFromPoint(e.clientX, e.clientY))?.checked?.(true);
|
||||||
|
|
||||||
// Drop selected messages on folder
|
const uids = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails();
|
||||||
ko.bindingHandlers.dropmessages = {
|
dragImage.querySelector('.text').textContent = uids.size;
|
||||||
init: (element, fValueAccessor) => {
|
|
||||||
const folder = fValueAccessor(),
|
|
||||||
// folder = ko.dataFor(element),
|
|
||||||
fnStop = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
element.classList.remove('droppableHover');
|
|
||||||
dragTimer.stop();
|
|
||||||
},
|
|
||||||
fnHover = e => {
|
|
||||||
if ('messages' === getDragAction(e)) {
|
|
||||||
fnStop(e);
|
|
||||||
element.classList.add('droppableHover');
|
|
||||||
if (folder && folder.collapsed()) {
|
|
||||||
dragTimer.start(() => {
|
|
||||||
folder.collapsed(false);
|
|
||||||
rl.app.setExpandedFolder(folder.fullName, true);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
element.addEventListener("dragenter", fnHover);
|
|
||||||
element.addEventListener("dragover", fnHover);
|
|
||||||
element.addEventListener("dragleave", fnStop);
|
|
||||||
element.addEventListener("drop", e => {
|
|
||||||
fnStop(e);
|
|
||||||
if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
|
|
||||||
let data = dragData.data;
|
|
||||||
if (folder && data && data.folder && isArray(data.uids)) {
|
|
||||||
rl.app.moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.sortableItem = {
|
// Make sure Chrome shows it
|
||||||
init: (element, fValueAccessor) => {
|
dragImage.style.left = e.clientX + 'px';
|
||||||
let options = ko.unwrap(fValueAccessor()) || {},
|
dragImage.style.top = e.clientY + 'px';
|
||||||
parent = element.parentNode,
|
dragImage.style.right = 'auto';
|
||||||
fnHover = e => {
|
|
||||||
if ('sortable' === getDragAction(e)) {
|
setDragAction(e, 'messages', 'copyMove', uids, dragImage);
|
||||||
|
|
||||||
|
// Remove the Chrome visibility
|
||||||
|
dragImage.style.cssText = '';
|
||||||
|
|
||||||
|
leftPanelDisabled(false);
|
||||||
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
|
|
||||||
if (node && node !== dragData.data && parent.contains(node)) {
|
|
||||||
let rect = node.getBoundingClientRect();
|
|
||||||
if (rect.top + (rect.height / 2) <= e.clientY) {
|
|
||||||
if (node.nextElementSibling !== dragData.data) {
|
|
||||||
node.after(dragData.data);
|
|
||||||
}
|
|
||||||
} else if (node.previousElementSibling !== dragData.data) {
|
|
||||||
node.before(dragData.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
element.addEventListener("dragstart", e => {
|
|
||||||
dragData = {
|
|
||||||
action: 'sortable',
|
|
||||||
element: element
|
|
||||||
};
|
|
||||||
setDragAction(e, 'sortable', 'move', element, element);
|
|
||||||
element.style.opacity = 0.25;
|
|
||||||
});
|
|
||||||
element.addEventListener("dragend", e => {
|
|
||||||
element.style.opacity = null;
|
|
||||||
if ('sortable' === getDragAction(e)) {
|
|
||||||
dragData.data.style.cssText = '';
|
|
||||||
let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
|
|
||||||
if (row != dragData.data) {
|
|
||||||
row.before(dragData.data);
|
|
||||||
}
|
|
||||||
dragData = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!parent.sortable) {
|
|
||||||
parent.sortable = true;
|
|
||||||
parent.addEventListener("dragenter", fnHover);
|
|
||||||
parent.addEventListener("dragover", fnHover);
|
|
||||||
parent.addEventListener("drop", e => {
|
|
||||||
if ('sortable' === getDragAction(e)) {
|
|
||||||
e.preventDefault();
|
|
||||||
let data = ko.dataFor(dragData.data),
|
|
||||||
from = options.list.indexOf(data),
|
|
||||||
to = [...parent.children].indexOf(dragData.data);
|
|
||||||
if (from != to) {
|
|
||||||
let arr = options.list();
|
|
||||||
arr.splice(to, 0, ...arr.splice(from, 1));
|
|
||||||
options.list(arr);
|
|
||||||
}
|
|
||||||
dragData = null;
|
|
||||||
options.afterMove && options.afterMove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}, false);
|
||||||
|
element.addEventListener("dragend", () => dragData = null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Drop selected messages on folder
|
||||||
|
dropmessages: {
|
||||||
|
init: (element, fValueAccessor) => {
|
||||||
|
const folder = fValueAccessor(); // ko.dataFor(element)
|
||||||
|
folder && addEventsListeners(element, {
|
||||||
|
dragenter: e => dragEnter(e, element, folder),
|
||||||
|
dragover: e => e.preventDefault(),
|
||||||
|
dragleave: e => dragStop(e, element),
|
||||||
|
drop: e => dragDrop(e, element, folder, dragData)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.initDom = {
|
sortableItem: {
|
||||||
init: (element, fValueAccessor) => fValueAccessor()(element)
|
init: (element, fValueAccessor) => {
|
||||||
};
|
let options = ko.unwrap(fValueAccessor()) || {},
|
||||||
|
parent = element.parentNode,
|
||||||
ko.bindingHandlers.onEsc = {
|
fnHover = e => {
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
if (dragSortable()) {
|
||||||
let fn = event => {
|
e.preventDefault();
|
||||||
if ('Escape' == event.key) {
|
let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
|
||||||
element.dispatchEvent(new Event('change'));
|
if (node && node !== dragData.data && parent.contains(node)) {
|
||||||
fValueAccessor().call(viewModel);
|
let rect = node.getBoundingClientRect();
|
||||||
|
if (rect.top + (rect.height / 2) <= e.clientY) {
|
||||||
|
if (node.nextElementSibling !== dragData.data) {
|
||||||
|
node.after(dragData.data);
|
||||||
|
}
|
||||||
|
} else if (node.previousElementSibling !== dragData.data) {
|
||||||
|
node.before(dragData.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addEventsListeners(element, {
|
||||||
|
dragstart: e => {
|
||||||
|
setDragAction(e, 'sortable', 'move', element, element);
|
||||||
|
element.style.opacity = 0.25;
|
||||||
|
},
|
||||||
|
dragend: () => {
|
||||||
|
element.style.opacity = null;
|
||||||
|
if (dragSortable()) {
|
||||||
|
dragData.data.style.cssText = '';
|
||||||
|
let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
|
||||||
|
if (row != dragData.data) {
|
||||||
|
row.before(dragData.data);
|
||||||
|
}
|
||||||
|
dragData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!parent.sortable) {
|
||||||
|
parent.sortable = true;
|
||||||
|
addEventsListeners(parent, {
|
||||||
|
dragenter: fnHover,
|
||||||
|
dragover: fnHover,
|
||||||
|
drop: e => {
|
||||||
|
if (dragSortable()) {
|
||||||
|
e.preventDefault();
|
||||||
|
let data = ko.dataFor(dragData.data),
|
||||||
|
from = options.list.indexOf(data),
|
||||||
|
to = [...parent.children].indexOf(dragData.data);
|
||||||
|
if (from != to) {
|
||||||
|
let arr = options.list();
|
||||||
|
arr.splice(to, 0, ...arr.splice(from, 1));
|
||||||
|
options.list(arr);
|
||||||
|
}
|
||||||
|
dragData = null;
|
||||||
|
options.afterMove?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
element.addEventListener('keyup', fn);
|
},
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.registerBootstrapDropdown = {
|
initDom: {
|
||||||
init: element => {
|
init: (element, fValueAccessor) => fValueAccessor()(element)
|
||||||
rl.Dropdowns.register(element);
|
},
|
||||||
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.openDropdownTrigger = {
|
registerBootstrapDropdown: {
|
||||||
update: (element, fValueAccessor) => {
|
init: element => {
|
||||||
if (ko.unwrap(fValueAccessor())) {
|
dropdowns.push(element);
|
||||||
const el = element.ddBtn;
|
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
|
||||||
el.open || el.toggle();
|
|
||||||
// el.focus();
|
|
||||||
|
|
||||||
rl.Dropdowns.detectVisibility();
|
|
||||||
fValueAccessor()(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
|
||||||
40
dev/External/ifvisible.js
vendored
40
dev/External/ifvisible.js
vendored
|
|
@ -1,40 +0,0 @@
|
||||||
(doc => {
|
|
||||||
let idle = 'idle',
|
|
||||||
visible = 'visible',
|
|
||||||
status = visible,
|
|
||||||
timer = 0,
|
|
||||||
wakeUp = () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
status = visible;
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
if (status === visible) {
|
|
||||||
status = idle;
|
|
||||||
dispatchEvent(new CustomEvent(idle));
|
|
||||||
}
|
|
||||||
}, 10000);
|
|
||||||
},
|
|
||||||
init = () => {
|
|
||||||
init = () => 0;
|
|
||||||
// Safari
|
|
||||||
addEventListener('pagehide', () => status = 'hidden');
|
|
||||||
// Else
|
|
||||||
doc.addEventListener('visibilitychange', () => {
|
|
||||||
status = doc.visibilityState;
|
|
||||||
doc.hidden || wakeUp();
|
|
||||||
});
|
|
||||||
wakeUp();
|
|
||||||
['mousemove','keyup','touchstart'].forEach(t => doc.addEventListener(t, wakeUp));
|
|
||||||
['scroll','pageshow'].forEach(t => addEventListener(t, wakeUp));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ifvisible = {
|
|
||||||
idle: callback => {
|
|
||||||
init();
|
|
||||||
addEventListener(idle, callback);
|
|
||||||
},
|
|
||||||
now: () => {
|
|
||||||
init();
|
|
||||||
return status === visible;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})(document);
|
|
||||||
283
dev/External/ko.js
vendored
283
dev/External/ko.js
vendored
|
|
@ -1,174 +1,169 @@
|
||||||
|
import ko from 'ko';
|
||||||
import { i18nToNodes } from 'Common/Translator';
|
import { i18nToNodes } from 'Common/Translator';
|
||||||
import { doc, createElement } from 'Common/Globals';
|
import { doc, createElement } from 'Common/Globals';
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
import { arrayLength, isFunction } from 'Common/Utils';
|
import { isFunction, forEachObjectEntry } from 'Common/Utils';
|
||||||
|
|
||||||
/**
|
export const
|
||||||
* The value of the pureComputed observable shouldn’t vary based on the
|
errorTip = (element, value) => value
|
||||||
* number of evaluations or other “hidden” information. Its value should be
|
? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100)
|
||||||
* based solely on the values of other observables in the application
|
: element.removeAttribute('data-rainloopErrorTip'),
|
||||||
*/
|
|
||||||
export const koComputable = fn => ko.computed(fn, {'pure':true});
|
|
||||||
|
|
||||||
ko.bindingHandlers.tooltipErrorTip = {
|
/**
|
||||||
init: (element, fValueAccessor) => {
|
* The value of the pureComputed observable shouldn’t vary based on the
|
||||||
doc.addEventListener('click', () => {
|
* number of evaluations or other “hidden” information. Its value should be
|
||||||
let value = fValueAccessor();
|
* based solely on the values of other observables in the application
|
||||||
ko.isObservable(value) && !ko.isComputed(value) && value('');
|
*/
|
||||||
element.removeAttribute('data-rainloopErrorTip');
|
koComputable = fn => ko.computed(fn, {'pure':true}),
|
||||||
});
|
// koObservable = value => ko.observable(value),
|
||||||
|
|
||||||
|
addObservablesTo = (target, observables) =>
|
||||||
|
forEachObjectEntry(observables, (key, value) =>
|
||||||
|
target[key] || (target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value)) ),
|
||||||
|
|
||||||
|
addComputablesTo = (target, computables) =>
|
||||||
|
forEachObjectEntry(computables, (key, fn) => target[key] = koComputable(fn)),
|
||||||
|
|
||||||
|
addSubscribablesTo = (target, subscribables) =>
|
||||||
|
forEachObjectEntry(subscribables, (key, fn) => target[key].subscribe(fn)),
|
||||||
|
|
||||||
|
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
|
||||||
|
|
||||||
|
onEvent = (element, event, fn) => {
|
||||||
|
element.addEventListener(event, fn);
|
||||||
|
ko.addDisposeCallback(element, () => element.removeEventListener(event, fn));
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
let value = ko.unwrap(fValueAccessor());
|
|
||||||
value = isFunction(value) ? value() : value;
|
|
||||||
if (value) {
|
|
||||||
setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100);
|
|
||||||
} else {
|
|
||||||
element.removeAttribute('data-rainloopErrorTip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.onEnter = {
|
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
|
||||||
let fn = event => {
|
let fn = event => {
|
||||||
if ('Enter' == event.key) {
|
if (key == event.key) {
|
||||||
element.dispatchEvent(new Event('change'));
|
// stopEvent(event);
|
||||||
fValueAccessor().call(viewModel);
|
// element.dispatchEvent(new Event('change'));
|
||||||
|
fValueAccessor().call(model);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
element.addEventListener('keydown', fn);
|
onEvent(element, 'keydown', fn);
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
},
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.onSpace = {
|
// With this we don't need delegateRunOnDestroy
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
koArrayWithDestroy = data => {
|
||||||
let fn = event => {
|
data = ko.observableArray(data);
|
||||||
if (' ' == event.key) {
|
data.subscribe(changes =>
|
||||||
fValueAccessor().call(viewModel, event);
|
changes.forEach(item =>
|
||||||
|
'deleted' === item.status && null == item.moved && item.value.onDestroy?.()
|
||||||
|
)
|
||||||
|
, data, 'arrayChange');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(ko.bindingHandlers, {
|
||||||
|
tooltipErrorTip: {
|
||||||
|
init: (element, fValueAccessor) => {
|
||||||
|
doc.addEventListener('click', () => {
|
||||||
|
let value = fValueAccessor();
|
||||||
|
ko.isObservable(value) && !ko.isComputed(value) && value('');
|
||||||
|
errorTip(element);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
update: (element, fValueAccessor) => {
|
||||||
|
let value = ko.unwrap(fValueAccessor());
|
||||||
|
errorTip(element, isFunction(value) ? value() : value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onEnter: {
|
||||||
|
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||||
|
onKey('Enter', element, fValueAccessor, fAllBindings, model)
|
||||||
|
},
|
||||||
|
|
||||||
|
onEsc: {
|
||||||
|
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||||
|
onKey('Escape', element, fValueAccessor, fAllBindings, model)
|
||||||
|
},
|
||||||
|
|
||||||
|
onSpace: {
|
||||||
|
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||||
|
onKey(' ', element, fValueAccessor, fAllBindings, model)
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle: {
|
||||||
|
init: (element, fValueAccessor) => {
|
||||||
|
let observable = fValueAccessor(),
|
||||||
|
fn = () => observable(!observable());
|
||||||
|
onEvent(element, 'click', fn);
|
||||||
|
onEvent(element, 'keydown', event => ' ' == event.key && fn());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
i18nUpdate: {
|
||||||
|
update: (element, fValueAccessor) => {
|
||||||
|
ko.unwrap(fValueAccessor());
|
||||||
|
i18nToNodes(element);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
command: {
|
||||||
|
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
|
||||||
|
const command = fValueAccessor();
|
||||||
|
|
||||||
|
if (!command || !command.canExecute) {
|
||||||
|
throw Error('Value should be a command');
|
||||||
}
|
}
|
||||||
};
|
|
||||||
element.addEventListener('keyup', fn);
|
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.i18nInit = {
|
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
||||||
init: element => i18nToNodes(element)
|
element,
|
||||||
};
|
fValueAccessor,
|
||||||
|
fAllBindings,
|
||||||
|
viewModel,
|
||||||
|
bindingContext
|
||||||
|
);
|
||||||
|
},
|
||||||
|
update: (element, fValueAccessor) => {
|
||||||
|
let disabled = !fValueAccessor().canExecute();
|
||||||
|
element.classList.toggle('disabled', disabled);
|
||||||
|
|
||||||
ko.bindingHandlers.i18nUpdate = {
|
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
|
||||||
update: (element, fValueAccessor) => {
|
element.disabled = disabled;
|
||||||
ko.unwrap(fValueAccessor());
|
}
|
||||||
i18nToNodes(element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.title = {
|
|
||||||
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.command = {
|
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
|
|
||||||
const command = fValueAccessor();
|
|
||||||
|
|
||||||
if (!command || !command.enabled || !command.canExecute) {
|
|
||||||
throw new Error('Value should be a command');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
|
||||||
element,
|
|
||||||
fValueAccessor,
|
|
||||||
fAllBindings,
|
|
||||||
viewModel,
|
|
||||||
bindingContext
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
const cl = element.classList,
|
|
||||||
command = fValueAccessor();
|
|
||||||
|
|
||||||
let disabled = !command.enabled();
|
saveTrigger: {
|
||||||
|
init: (element) => {
|
||||||
disabled = disabled || !command.canExecute();
|
let icon = element;
|
||||||
cl.toggle('disabled', disabled);
|
if (element.matches('input,select,textarea')) {
|
||||||
|
element.classList.add('settings-save-trigger-input');
|
||||||
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
|
element.after(element.saveTriggerIcon = icon = createElement('span'));
|
||||||
element.disabled = disabled;
|
}
|
||||||
|
icon.classList.add('settings-save-trigger');
|
||||||
|
},
|
||||||
|
update: (element, fValueAccessor) => {
|
||||||
|
const value = parseInt(ko.unwrap(fValueAccessor()),10);
|
||||||
|
let cl = (element.saveTriggerIcon || element).classList;
|
||||||
|
if (element.saveTriggerIcon) {
|
||||||
|
cl.toggle('saving', value === SaveSettingStatus.Saving);
|
||||||
|
cl.toggle('success', value === SaveSettingStatus.Success);
|
||||||
|
cl.toggle('error', value === SaveSettingStatus.Failed);
|
||||||
|
}
|
||||||
|
cl = element.classList;
|
||||||
|
cl.toggle('success', value === SaveSettingStatus.Success);
|
||||||
|
cl.toggle('error', value === SaveSettingStatus.Failed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
ko.bindingHandlers.saveTrigger = {
|
|
||||||
init: (element) => {
|
|
||||||
let icon = element;
|
|
||||||
if (element.matches('input,select,textarea')) {
|
|
||||||
element.classList.add('settings-saved-trigger-input');
|
|
||||||
element.after(element.saveTriggerIcon = icon = createElement('span'));
|
|
||||||
}
|
|
||||||
icon.classList.add('settings-save-trigger');
|
|
||||||
},
|
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
const value = parseInt(ko.unwrap(fValueAccessor()),10);
|
|
||||||
let cl = (element.saveTriggerIcon || element).classList;
|
|
||||||
if (element.saveTriggerIcon) {
|
|
||||||
cl.toggle('saving', value === SaveSettingsStep.Animate);
|
|
||||||
cl.toggle('success', value === SaveSettingsStep.TrueResult);
|
|
||||||
cl.toggle('error', value === SaveSettingsStep.FalseResult);
|
|
||||||
}
|
|
||||||
cl = element.classList;
|
|
||||||
cl.toggle('success', value === SaveSettingsStep.TrueResult);
|
|
||||||
cl.toggle('error', value === SaveSettingsStep.FalseResult);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// extenders
|
// extenders
|
||||||
|
|
||||||
ko.extenders.limitedList = (target, limitedList) => {
|
|
||||||
const result = ko
|
|
||||||
.computed({
|
|
||||||
read: target,
|
|
||||||
write: (newValue) => {
|
|
||||||
const currentValue = ko.unwrap(target),
|
|
||||||
list = ko.unwrap(limitedList);
|
|
||||||
|
|
||||||
if (arrayLength(list)) {
|
|
||||||
if (list.includes(newValue)) {
|
|
||||||
target(newValue);
|
|
||||||
} else if (list.includes(currentValue, list)) {
|
|
||||||
target(currentValue + ' ');
|
|
||||||
target(currentValue);
|
|
||||||
} else {
|
|
||||||
target(list[0] + ' ');
|
|
||||||
target(list[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
target('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.extend({ notify: 'always' });
|
|
||||||
|
|
||||||
result(target());
|
|
||||||
|
|
||||||
if (!result.valueHasMutated) {
|
|
||||||
result.valueHasMutated = () => target.valueHasMutated();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.extenders.toggleSubscribeProperty = (target, options) => {
|
ko.extenders.toggleSubscribeProperty = (target, options) => {
|
||||||
const prop = options[1];
|
const prop = options[1];
|
||||||
if (prop) {
|
if (prop) {
|
||||||
target.subscribe(
|
target.subscribe(
|
||||||
prev => prev && prev[prop] && prev[prop](false),
|
prev => prev?.[prop]?.(false),
|
||||||
options[0],
|
options[0],
|
||||||
'beforeChange'
|
'beforeChange'
|
||||||
);
|
);
|
||||||
|
|
||||||
target.subscribe(next => next && next[prop] && next[prop](true), options[0]);
|
target.subscribe(next => next?.[prop]?.(true), options[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
|
|
@ -181,6 +176,6 @@ ko.extenders.falseTimeout = (target, option) => {
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
|
|
||||||
ko.observable.fn.askDeleteHelper = function() {
|
ko.observable.fn.askDeleteHelper = function(prop='askDelete') {
|
||||||
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
|
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, prop] });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<title></title>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: rgba(125,128,128,0.3);
|
|
||||||
border-bottom: 1px solid #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
header * {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header time {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
border-left: 2px solid rgba(125,128,128,0.5);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body > * {
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
import { isArray, isFunction, addObservablesTo, addComputablesTo, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
import { isArray, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
||||||
|
import { dispose, addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
function dispose(disposable) {
|
|
||||||
if (disposable && isFunction(disposable.dispose)) {
|
|
||||||
disposable.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeCast(curValue, newValue) {
|
function typeCast(curValue, newValue) {
|
||||||
if (null != curValue) {
|
if (null != curValue) {
|
||||||
switch (typeof curValue)
|
switch (typeof curValue)
|
||||||
{
|
{
|
||||||
case 'boolean': return 0 != newValue && !!newValue;
|
case 'boolean':
|
||||||
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
|
return 0 != newValue && !!newValue;
|
||||||
case 'string': return null != newValue ? '' + newValue : '';
|
case 'number':
|
||||||
|
newValue = parseFloat(newValue);
|
||||||
|
return isFinite(newValue) ? newValue : 0;
|
||||||
|
case 'string':
|
||||||
|
return null != newValue ? '' + newValue : '';
|
||||||
case 'object':
|
case 'object':
|
||||||
if (curValue.constructor.reviveFromJson) {
|
if (curValue.constructor.reviveFromJson) {
|
||||||
return curValue.constructor.reviveFromJson(newValue);
|
return curValue.constructor.reviveFromJson(newValue);
|
||||||
|
|
@ -28,10 +27,10 @@ export class AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractModel) {
|
if (new.target === AbstractModel) {
|
||||||
throw new Error("Can't instantiate AbstractModel!");
|
throw Error("Can't instantiate AbstractModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
this.subscribables = [];
|
Object.defineProperty(this, 'disposables', {value: []});
|
||||||
}
|
}
|
||||||
|
|
||||||
addObservables(observables) {
|
addObservables(observables) {
|
||||||
|
|
@ -43,25 +42,25 @@ export class AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
addSubscribables(subscribables) {
|
addSubscribables(subscribables) {
|
||||||
forEachObjectEntry(subscribables, (key, fn) => this.subscribables.push( this[key].subscribe(fn) ) );
|
// addSubscribablesTo(this, subscribables);
|
||||||
|
forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called by delegateRunOnDestroy */
|
/** Called by delegateRunOnDestroy */
|
||||||
onDestroy() {
|
onDestroy() {
|
||||||
/** dispose ko subscribables */
|
/** dispose ko subscribables */
|
||||||
this.subscribables.forEach(dispose);
|
this.disposables.forEach(dispose);
|
||||||
/** clear object entries */
|
/** clear object entries */
|
||||||
// forEachObjectEntry(this, (key, value) => {
|
// forEachObjectEntry(this, (key, value) => {
|
||||||
forEachObjectValue(this, value => {
|
forEachObjectValue(this, value => {
|
||||||
/** clear CollectionModel */
|
/** clear CollectionModel */
|
||||||
let arr = ko.isObservableArray(value) ? value() : value;
|
(ko.isObservableArray(value) ? value() : value)?.onDestroy?.();
|
||||||
arr && arr.onDestroy && value.onDestroy();
|
|
||||||
/** destroy ko.observable/ko.computed? */
|
/** destroy ko.observable/ko.computed? */
|
||||||
dispose(value);
|
// dispose(value);
|
||||||
/** clear object value */
|
/** clear object value */
|
||||||
// this[key] = null; // TODO: issue with Contacts view
|
// this[key] = null; // TODO: issue with Contacts view
|
||||||
});
|
});
|
||||||
// this.subscribables = [];
|
// this.disposables = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -80,18 +79,16 @@ export class AbstractModel {
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(json) {
|
static reviveFromJson(json) {
|
||||||
let obj = this.validJson(json) ? new this() : null;
|
let obj = this.validJson(json) ? new this() : null;
|
||||||
obj && obj.revivePropertiesFromJson(json);
|
obj?.revivePropertiesFromJson(json);
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
revivePropertiesFromJson(json) {
|
revivePropertiesFromJson(json) {
|
||||||
let model = this.constructor;
|
const model = this.constructor,
|
||||||
if (!model.validJson(json)) {
|
valid = model.validJson(json);
|
||||||
return false;
|
valid && forEachObjectEntry(json, (key, value) => {
|
||||||
}
|
|
||||||
forEachObjectEntry(json, (key, value) => {
|
|
||||||
if ('@' !== key[0]) try {
|
if ('@' !== key[0]) try {
|
||||||
key = key[0].toLowerCase() + key.slice(1);
|
// key = key[0].toLowerCase() + key.slice(1);
|
||||||
switch (typeof this[key])
|
switch (typeof this[key])
|
||||||
{
|
{
|
||||||
case 'function':
|
case 'function':
|
||||||
|
|
@ -107,9 +104,12 @@ export class AbstractModel {
|
||||||
case 'string':
|
case 'string':
|
||||||
this[key] = typeCast(this[key], value);
|
this[key] = typeCast(this[key], value);
|
||||||
break;
|
break;
|
||||||
// fall through
|
|
||||||
case 'undefined':
|
case 'undefined':
|
||||||
default:
|
console.log(`Undefined ${model.name}.${key} set`);
|
||||||
|
this[key] = value;
|
||||||
|
break;
|
||||||
|
// default:
|
||||||
|
// console.log((typeof this[key])+` ${model.name}.${key} not revived`);
|
||||||
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -117,7 +117,7 @@ export class AbstractModel {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,14 @@ export class AbstractScreen {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onStart() {
|
onStart() {
|
||||||
if (!this.__started) {
|
const routes = this.routes();
|
||||||
this.__started = true;
|
if (arrayLength(routes)) {
|
||||||
const routes = this.routes();
|
let route = new Crossroads(),
|
||||||
if (arrayLength(routes)) {
|
fMatcher = (this.onRoute || (()=>0)).bind(this);
|
||||||
let route = new Crossroads(),
|
|
||||||
fMatcher = (this.onRoute || (()=>0)).bind(this);
|
|
||||||
|
|
||||||
routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1]));
|
routes.forEach(item => item && (route.addRoute(item[0], fMatcher).rules = item[1]));
|
||||||
|
|
||||||
this.__cross = route;
|
this.__cross = route;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { inFocus, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils';
|
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
import { Scope } from 'Common/Enums';
|
import { keyScope, addShortcut, SettingsGet, toggleLeftPanel, elementById } from 'Common/Globals';
|
||||||
import { keyScope, Settings, leftPanelDisabled } from 'Common/Globals';
|
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { ViewType, showScreenPopup } from 'Knoin/Knoin';
|
|
||||||
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
|
|
||||||
class AbstractView {
|
class AbstractView {
|
||||||
constructor(templateID, type)
|
constructor(templateID, type)
|
||||||
{
|
{
|
||||||
// Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
|
// Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
|
||||||
this.viewModelTemplateID = templateID;
|
this.viewModelTemplateID = templateID || this.constructor.name.replace('UserView', '');
|
||||||
this.viewType = type;
|
this.viewType = type;
|
||||||
this.viewModelDom = null;
|
this.viewModelDom = null;
|
||||||
|
|
||||||
this.keyScope = {
|
this.keyScope = {
|
||||||
scope: Scope.None,
|
scope: 'none',
|
||||||
previous: Scope.None,
|
previous: 'none',
|
||||||
set: function() {
|
set: function() {
|
||||||
this.previous = keyScope();
|
this.previous = keyScope();
|
||||||
keyScope(this.scope);
|
keyScope(this.scope);
|
||||||
|
|
@ -28,8 +29,8 @@ class AbstractView {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
onBuild() {}
|
onBuild() {}
|
||||||
onBeforeShow() {}
|
beforeShow() {} // Happens before: hidden = false
|
||||||
onShow() {}
|
onShow() {} // Happens after: hidden = false
|
||||||
onHide() {}
|
onHide() {}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -55,37 +56,29 @@ export class AbstractViewPopup extends AbstractView
|
||||||
{
|
{
|
||||||
constructor(name)
|
constructor(name)
|
||||||
{
|
{
|
||||||
super('Popups' + name, ViewType.Popup);
|
super('Popups' + name, ViewTypePopup);
|
||||||
if (name in Scope) {
|
this.keyScope.scope = name;
|
||||||
this.keyScope.scope = Scope[name];
|
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
||||||
}
|
this.close = () => this.modalVisible(false);
|
||||||
this.bDisabeCloseOnEsc = false;
|
this.tryToClose = () => (false === this.onClose()) || this.close();
|
||||||
this.modalVisibility = ko.observable(false).extend({ rateLimit: 0 });
|
addShortcut('escape,close', '', name, () => {
|
||||||
}
|
this.modalVisible() && this.tryToClose();
|
||||||
/*
|
return false;
|
||||||
onShowWithDelay() {}
|
// return true; Issue with supported modal close
|
||||||
onHideWithDelay() {}
|
|
||||||
|
|
||||||
cancelCommand() {}
|
|
||||||
closeCommand() {}
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
registerPopupKeyDown() {
|
|
||||||
addEventListener('keydown', event => {
|
|
||||||
if (event && this.modalVisibility()) {
|
|
||||||
if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) {
|
|
||||||
this.cancelCommand();
|
|
||||||
return false;
|
|
||||||
} else if ('Backspace' == event.key && !inFocus()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Happens when user hits Escape or Close key
|
||||||
|
// return false to prevent closing
|
||||||
|
onClose() {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
beforeShow() {} // Happens before showModal()
|
||||||
|
onShow() {} // Happens after showModal()
|
||||||
|
afterShow() {} // Happens after showModal() animation transitionend
|
||||||
|
onHide() {} // Happens before animation transitionend
|
||||||
|
afterHide() {} // Happens after animation transitionend
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
AbstractViewPopup.showModal = function(params = []) {
|
AbstractViewPopup.showModal = function(params = []) {
|
||||||
|
|
@ -93,23 +86,15 @@ AbstractViewPopup.showModal = function(params = []) {
|
||||||
}
|
}
|
||||||
|
|
||||||
AbstractViewPopup.hidden = function() {
|
AbstractViewPopup.hidden = function() {
|
||||||
return !this.__vm || !this.__vm.modalVisibility();
|
return !this.__vm || !this.__vm.modalVisible();
|
||||||
}
|
|
||||||
|
|
||||||
export class AbstractViewCenter extends AbstractView
|
|
||||||
{
|
|
||||||
constructor(templateID)
|
|
||||||
{
|
|
||||||
super(templateID, ViewType.Content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractViewLeft extends AbstractView
|
export class AbstractViewLeft extends AbstractView
|
||||||
{
|
{
|
||||||
constructor(templateID)
|
constructor(templateID)
|
||||||
{
|
{
|
||||||
super(templateID, ViewType.Left);
|
super(templateID, 'left');
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
this.toggleLeftPanel = toggleLeftPanel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,25 +102,64 @@ export class AbstractViewRight extends AbstractView
|
||||||
{
|
{
|
||||||
constructor(templateID)
|
constructor(templateID)
|
||||||
{
|
{
|
||||||
super(templateID, ViewType.Right);
|
super(templateID, 'right');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
export class AbstractViewSettings
|
export class AbstractViewSettings
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
onBuild(viewModelDom) {}
|
onBuild(viewModelDom) {}
|
||||||
onBeforeShow() {}
|
beforeShow() {}
|
||||||
onShow() {}
|
onShow() {}
|
||||||
onHide() {}
|
onHide() {}
|
||||||
viewModelDom
|
viewModelDom
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* When this[name] does not exists, create as observable with value of SettingsGet(name)
|
||||||
|
* When this[name+'Trigger'] does not exists, create as observable
|
||||||
|
* Subscribe to this[name], and handle saving the setting
|
||||||
|
*/
|
||||||
|
addSetting(name, valueCb)
|
||||||
|
{
|
||||||
|
let prop = name[0].toLowerCase() + name.slice(1),
|
||||||
|
trigger = prop + 'Trigger';
|
||||||
|
addObservablesTo(this, {
|
||||||
|
[prop]: SettingsGet(name),
|
||||||
|
[trigger]: SaveSettingStatus.Idle,
|
||||||
|
});
|
||||||
|
addSubscribablesTo(this, {
|
||||||
|
[prop]: (value => {
|
||||||
|
this[trigger](SaveSettingStatus.Saving);
|
||||||
|
valueCb?.(value);
|
||||||
|
rl.app.Remote.saveSetting(name, value,
|
||||||
|
iError => {
|
||||||
|
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
|
||||||
|
// iError || Settings.set(name, value);
|
||||||
|
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).debounce(999),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export class AbstractViewLogin extends AbstractViewCenter {
|
/**
|
||||||
|
* Foreach name if this[name] does not exists, create as observable with value of SettingsGet(name)
|
||||||
|
* Subscribe to this[name], for saving the setting
|
||||||
|
*/
|
||||||
|
addSettings(names)
|
||||||
|
{
|
||||||
|
names.forEach(name => {
|
||||||
|
let prop = name[0].toLowerCase() + name.slice(1);
|
||||||
|
this[prop] || (this[prop] = ko.observable(SettingsGet(name)));
|
||||||
|
this[prop].subscribe(value => rl.app.Remote.saveSetting(name, value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbstractViewLogin extends AbstractView {
|
||||||
constructor(templateID) {
|
constructor(templateID) {
|
||||||
super(templateID);
|
super(templateID, 'content');
|
||||||
this.hideSubmitButton = Settings.app('hideSubmitButton');
|
|
||||||
this.formError = ko.observable(false).extend({ falseTimeout: 500 });
|
this.formError = ko.observable(false).extend({ falseTimeout: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,9 +168,16 @@ export class AbstractViewLogin extends AbstractViewCenter {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
elementById('rl-left').hidden = true;
|
||||||
|
elementById('rl-right').hidden = true;
|
||||||
rl.route.off();
|
rl.route.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onHide() {
|
||||||
|
elementById('rl-left').hidden = false;
|
||||||
|
elementById('rl-right').hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
submitForm() {
|
submitForm() {
|
||||||
// return false;
|
// return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { koComputable } from 'External/ko';
|
import { koComputable } from 'External/ko';
|
||||||
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
|
import { doc, $htmlCL, elementById, createElement, fireEvent } from 'Common/Globals';
|
||||||
import { isFunction, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
import { forEachObjectEntry } from 'Common/Utils';
|
||||||
|
import { i18nToNodes } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { leftPanelDisabled } from 'Common/Globals';
|
||||||
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
let
|
let
|
||||||
SCREENS = {},
|
|
||||||
currentScreen = null,
|
currentScreen = null,
|
||||||
defaultScreenName = '';
|
defaultScreenName = '';
|
||||||
|
|
||||||
const
|
const
|
||||||
autofocus = dom => {
|
SCREENS = new Map,
|
||||||
const af = dom.querySelector('[autofocus]');
|
|
||||||
af && af.focus();
|
autofocus = dom => dom.querySelector('[autofocus]')?.focus(),
|
||||||
},
|
|
||||||
|
|
||||||
visiblePopups = new Set,
|
visiblePopups = new Set,
|
||||||
|
|
||||||
|
|
@ -20,122 +22,110 @@ const
|
||||||
* @param {string} screenName
|
* @param {string} screenName
|
||||||
* @returns {?Object}
|
* @returns {?Object}
|
||||||
*/
|
*/
|
||||||
screen = screenName => screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null,
|
screen = screenName => (screenName && SCREENS.get(screenName)) || null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Creates the extended AbstractView model
|
||||||
* @param {Function} ViewModelClass
|
* @param {Function} ViewModelClass
|
||||||
* @param {Object=} vmScreen
|
* @param {Object=} vmScreen
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
buildViewModel = (ViewModelClass, vmScreen) => {
|
buildViewModel = (ViewModelClass, vmScreen) => {
|
||||||
if (ViewModelClass && !ViewModelClass.__builded) {
|
if (ViewModelClass && !ViewModelClass.__vm) {
|
||||||
let vmDom = null;
|
const
|
||||||
const vm = new ViewModelClass(vmScreen),
|
vm = new ViewModelClass(vmScreen),
|
||||||
position = vm.viewType || '',
|
id = vm.viewModelTemplateID,
|
||||||
dialog = ViewType.Popup === position,
|
position = 'rl-' + vm.viewType,
|
||||||
vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
|
dialog = ViewTypePopup === vm.viewType,
|
||||||
|
vmPlace = doc.getElementById(position);
|
||||||
ViewModelClass.__builded = true;
|
|
||||||
ViewModelClass.__vm = vm;
|
|
||||||
|
|
||||||
if (vmPlace) {
|
if (vmPlace) {
|
||||||
vmDom = dialog
|
ViewModelClass.__vm = vm;
|
||||||
? Element.fromHTML('<dialog id="V-'+ vm.viewModelTemplateID + '"></dialog>')
|
|
||||||
: Element.fromHTML('<div id="V-'+ vm.viewModelTemplateID + '" hidden=""></div>');
|
let vmDom = dialog
|
||||||
|
? createElement('dialog',{id:'V-'+id})
|
||||||
|
: createElement('div',{id:'V-'+id,hidden:''})
|
||||||
vmPlace.append(vmDom);
|
vmPlace.append(vmDom);
|
||||||
|
|
||||||
vm.viewModelDom = vmDom;
|
vm.viewModelDom = vmDom;
|
||||||
ViewModelClass.__dom = vmDom;
|
|
||||||
|
|
||||||
if (ViewType.Popup === position) {
|
if (dialog) {
|
||||||
vm.cancelCommand = vm.closeCommand = createCommand(() => hideScreenPopup(ViewModelClass));
|
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
||||||
|
|
||||||
// Firefox / Safari HTMLDialogElement not defined
|
|
||||||
if (!vmDom.showModal) {
|
if (!vmDom.showModal) {
|
||||||
vmDom.classList.add('polyfill');
|
vmDom.className = 'polyfill';
|
||||||
vmDom.showModal = () => {
|
vmDom.showModal = () => {
|
||||||
if (!vmDom.backdrop) {
|
vmDom.backdrop ||
|
||||||
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
|
vmDom.before(vmDom.backdrop = createElement('div',{class:'dialog-backdrop'}));
|
||||||
}
|
|
||||||
vmDom.setAttribute('open','');
|
vmDom.setAttribute('open','');
|
||||||
vmDom.open = true;
|
vmDom.open = true;
|
||||||
vmDom.returnValue = null;
|
|
||||||
vmDom.backdrop.hidden = false;
|
vmDom.backdrop.hidden = false;
|
||||||
};
|
};
|
||||||
vmDom.close = v => {
|
vmDom.close = () => {
|
||||||
vmDom.backdrop.hidden = true;
|
// if (vmDom.dispatchEvent(new CustomEvent('cancel', {cancelable:true}))) {
|
||||||
vmDom.returnValue = v;
|
vmDom.backdrop.hidden = true;
|
||||||
vmDom.removeAttribute('open', null);
|
vmDom.removeAttribute('open', null);
|
||||||
vmDom.open = false;
|
vmDom.open = false;
|
||||||
|
// vmDom.dispatchEvent(new CustomEvent('close'));
|
||||||
|
// }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/cancel_event
|
||||||
|
// vmDom.addEventListener('cancel', event => (false === vm.onClose() && event.preventDefault()));
|
||||||
|
// vmDom.addEventListener('close', () => vm.modalVisible(false));
|
||||||
|
|
||||||
// show/hide popup/modal
|
// show/hide popup/modal
|
||||||
|
// transitionend is called for each property, so we only listen to `opacity`
|
||||||
|
// as defined in CSS by `dialog:not(.animate)`
|
||||||
const endShowHide = e => {
|
const endShowHide = e => {
|
||||||
if (e.target === vmDom) {
|
if (e.target === vmDom && 'opacity' === e.propertyName) {
|
||||||
if (vmDom.classList.contains('animate')) {
|
if (vmDom.classList.contains('animate')) {
|
||||||
autofocus(vmDom);
|
vm.afterShow?.();
|
||||||
vm.onShowWithDelay && vm.onShowWithDelay();
|
fireEvent('rl-vm-visible', vm);
|
||||||
} else {
|
} else {
|
||||||
vmDom.close();
|
vmDom.close();
|
||||||
vm.onHideWithDelay && vm.onHideWithDelay();
|
vm.afterHide?.();
|
||||||
|
// fireEvent('rl-vm-hidden', vm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
vm.modalVisibility.subscribe(value => {
|
vm.modalVisible.subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
i18nToNodes(vmDom);
|
||||||
visiblePopups.add(vm);
|
visiblePopups.add(vm);
|
||||||
vmDom.style.zIndex = 3000 + (visiblePopups.size * 2);
|
vmDom.style.zIndex = 3001 + (visiblePopups.size * 2);
|
||||||
vmDom.showModal();
|
vmDom.showModal();
|
||||||
if (vmDom.backdrop) {
|
if (vmDom.backdrop) {
|
||||||
vmDom.backdrop.style.zIndex = 3000 + visiblePopups.size;
|
vmDom.backdrop.style.zIndex = 3000 + (visiblePopups.size * 2);
|
||||||
}
|
}
|
||||||
vm.keyScope.set();
|
vm.keyScope.set();
|
||||||
|
setTimeout(()=>autofocus(vmDom),1);
|
||||||
requestAnimationFrame(() => { // wait just before the next paint
|
requestAnimationFrame(() => { // wait just before the next paint
|
||||||
vmDom.offsetHeight; // force a reflow
|
vmDom.offsetHeight; // force a reflow
|
||||||
vmDom.classList.add('animate'); // trigger the transitions
|
vmDom.classList.add('animate'); // trigger the transitions
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
visiblePopups.delete(vm);
|
visiblePopups.delete(vm);
|
||||||
vm.onHide && vm.onHide();
|
vm.onHide?.();
|
||||||
vm.keyScope.unset();
|
vm.keyScope.unset();
|
||||||
vmDom.classList.remove('animate'); // trigger the transitions
|
vmDom.classList.remove('animate'); // trigger the transitions
|
||||||
}
|
}
|
||||||
arePopupsVisible(0 < visiblePopups.size);
|
arePopupsVisible(0 < visiblePopups.size);
|
||||||
/*
|
|
||||||
// the old ko.bindingHandlers.modal
|
|
||||||
const close = vmDom.querySelector('.close'),
|
|
||||||
click = () => vm.modalVisibility(false);
|
|
||||||
if (close) {
|
|
||||||
close.addEventListener('click.koModal', click);
|
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(vmDom, () =>
|
|
||||||
close.removeEventListener('click.koModal', click)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
if ('ontransitionend' in vmDom) {
|
vmDom.addEventListener('transitionend', endShowHide);
|
||||||
vmDom.addEventListener('transitionend', endShowHide);
|
|
||||||
} else {
|
|
||||||
// For Edge < 79 and mobile browsers
|
|
||||||
vm.modalVisibility.subscribe(() => ()=>setTimeout(endShowHide({target:vmDom}), 500));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fireEvent('rl-view-model.create', vm);
|
||||||
|
|
||||||
ko.applyBindingAccessorsToNode(
|
ko.applyBindingAccessorsToNode(
|
||||||
vmDom,
|
vmDom,
|
||||||
{
|
{
|
||||||
i18nInit: true,
|
template: () => ({ name: id })
|
||||||
template: () => ({ name: vm.viewModelTemplateID })
|
|
||||||
},
|
},
|
||||||
vm
|
vm
|
||||||
);
|
);
|
||||||
|
|
||||||
vm.onBuild && vm.onBuild(vmDom);
|
vm.onBuild?.(vmDom);
|
||||||
if (vm && ViewType.Popup === position) {
|
|
||||||
vm.registerPopupKeyDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent('rl-view-model', vm);
|
fireEvent('rl-view-model', vm);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -143,38 +133,28 @@ const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ViewModelClass && ViewModelClass.__vm;
|
return ViewModelClass?.__vm;
|
||||||
},
|
},
|
||||||
|
|
||||||
forEachViewModel = (screen, fn) => {
|
forEachViewModel = (screen, fn) => {
|
||||||
screen.viewModels.forEach(ViewModelClass => {
|
screen.viewModels.forEach(ViewModelClass => {
|
||||||
if (
|
if (
|
||||||
ViewModelClass.__vm &&
|
ViewModelClass.__vm &&
|
||||||
ViewModelClass.__dom &&
|
ViewTypePopup !== ViewModelClass.__vm.viewType
|
||||||
ViewType.Popup !== ViewModelClass.__vm.viewType
|
|
||||||
) {
|
) {
|
||||||
fn(ViewModelClass.__vm, ViewModelClass.__dom);
|
fn(ViewModelClass.__vm, ViewModelClass.__vm.viewModelDom);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
hideScreen = (screenToHide, destroy) => {
|
hideScreen = (screenToHide, destroy) => {
|
||||||
screenToHide.onHide && screenToHide.onHide();
|
screenToHide.onHide?.();
|
||||||
forEachViewModel(screenToHide, (vm, dom) => {
|
forEachViewModel(screenToHide, (vm, dom) => {
|
||||||
dom.hidden = true;
|
dom.hidden = true;
|
||||||
vm.onHide && vm.onHide();
|
vm.onHide?.();
|
||||||
destroy && vm.viewModelDom.remove();
|
destroy && dom.remove();
|
||||||
});
|
});
|
||||||
},
|
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} ViewModelClassToHide
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
hideScreenPopup = ViewModelClassToHide => {
|
|
||||||
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
|
|
||||||
ViewModelClassToHide.__vm.modalVisibility(false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -183,20 +163,14 @@ const
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
screenOnRoute = (screenName, subPart) => {
|
screenOnRoute = (screenName, subPart) => {
|
||||||
let vmScreen = null,
|
screenName = screenName || defaultScreenName;
|
||||||
isSameScreen = false;
|
if (screenName && fireEvent('sm-show-screen', screenName + (subPart ? '/' + subPart : ''), 1)) {
|
||||||
|
// Close all popups
|
||||||
|
for (let vm of visiblePopups) {
|
||||||
|
vm.tryToClose();
|
||||||
|
}
|
||||||
|
|
||||||
if (null == screenName || '' == screenName) {
|
let vmScreen = screen(screenName);
|
||||||
screenName = defaultScreenName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all popups
|
|
||||||
for (let vm of visiblePopups) {
|
|
||||||
vm.closeCommand();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (screenName) {
|
|
||||||
vmScreen = screen(screenName);
|
|
||||||
if (!vmScreen) {
|
if (!vmScreen) {
|
||||||
vmScreen = screen(defaultScreenName);
|
vmScreen = screen(defaultScreenName);
|
||||||
if (vmScreen) {
|
if (vmScreen) {
|
||||||
|
|
@ -205,8 +179,8 @@ const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vmScreen && vmScreen.__started) {
|
if (vmScreen?.__started) {
|
||||||
isSameScreen = currentScreen && vmScreen === currentScreen;
|
let isSameScreen = currentScreen && vmScreen === currentScreen;
|
||||||
|
|
||||||
if (!vmScreen.__builded) {
|
if (!vmScreen.__builded) {
|
||||||
vmScreen.__builded = true;
|
vmScreen.__builded = true;
|
||||||
|
|
@ -215,32 +189,31 @@ const
|
||||||
buildViewModel(ViewModelClass, vmScreen)
|
buildViewModel(ViewModelClass, vmScreen)
|
||||||
);
|
);
|
||||||
|
|
||||||
vmScreen.onBuild && vmScreen.onBuild();
|
vmScreen.onBuild?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// hide screen
|
// hide screen
|
||||||
if (currentScreen && !isSameScreen) {
|
currentScreen && !isSameScreen && hideScreen(currentScreen);
|
||||||
hideScreen(currentScreen);
|
|
||||||
}
|
|
||||||
// --
|
// --
|
||||||
|
|
||||||
currentScreen = vmScreen;
|
currentScreen = vmScreen;
|
||||||
|
|
||||||
// show screen
|
// show screen
|
||||||
if (!isSameScreen) {
|
if (!isSameScreen) {
|
||||||
vmScreen.onShow && vmScreen.onShow();
|
vmScreen.onShow?.();
|
||||||
|
|
||||||
forEachViewModel(vmScreen, (vm, dom) => {
|
forEachViewModel(vmScreen, (vm, dom) => {
|
||||||
vm.onBeforeShow && vm.onBeforeShow();
|
vm.beforeShow?.();
|
||||||
|
i18nToNodes(dom);
|
||||||
dom.hidden = false;
|
dom.hidden = false;
|
||||||
vm.onShow && vm.onShow();
|
vm.onShow?.();
|
||||||
autofocus(dom);
|
autofocus(dom);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// --
|
// --
|
||||||
|
|
||||||
vmScreen.__cross && vmScreen.__cross.parse(subPart);
|
vmScreen.__cross?.parse(subPart);
|
||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -248,29 +221,7 @@ const
|
||||||
|
|
||||||
|
|
||||||
export const
|
export const
|
||||||
ViewType = {
|
ViewTypePopup = 'popups',
|
||||||
Popup: 'Popups',
|
|
||||||
Left: 'Left',
|
|
||||||
Right: 'Right',
|
|
||||||
Content: 'Content'
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} fExecute
|
|
||||||
* @param {(Function|boolean|null)=} fCanExecute = true
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
createCommand = (fExecute, fCanExecute) => {
|
|
||||||
let fResult = () => {
|
|
||||||
fResult.canExecute() && fExecute.call(null);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
fResult.enabled = ko.observable(true);
|
|
||||||
fResult.canExecute = isFunction(fCanExecute)
|
|
||||||
? koComputable(() => fResult.enabled() && fCanExecute())
|
|
||||||
: fResult.enabled;
|
|
||||||
return fResult;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} ViewModelClassToShow
|
* @param {Function} ViewModelClassToShow
|
||||||
|
|
@ -278,16 +229,12 @@ export const
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
showScreenPopup = (ViewModelClassToShow, params = []) => {
|
showScreenPopup = (ViewModelClassToShow, params = []) => {
|
||||||
const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
|
const vm = buildViewModel(ViewModelClassToShow);
|
||||||
|
|
||||||
if (vm) {
|
if (vm) {
|
||||||
params = params || [];
|
params = params || [];
|
||||||
|
vm.beforeShow?.(...params);
|
||||||
vm.onBeforeShow && vm.onBeforeShow(...params);
|
vm.modalVisible(true);
|
||||||
|
vm.onShow?.(...params);
|
||||||
vm.modalVisibility(true);
|
|
||||||
|
|
||||||
vm.onShow && vm.onShow(...params);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -299,24 +246,27 @@ export const
|
||||||
*/
|
*/
|
||||||
startScreens = screensClasses => {
|
startScreens = screensClasses => {
|
||||||
hasher.clear();
|
hasher.clear();
|
||||||
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
|
SCREENS.forEach(screen => hideScreen(screen, 1));
|
||||||
SCREENS = {};
|
SCREENS.clear();
|
||||||
currentScreen = null,
|
currentScreen = null,
|
||||||
defaultScreenName = '';
|
defaultScreenName = '';
|
||||||
|
|
||||||
screensClasses.forEach(CScreen => {
|
screensClasses.forEach(CScreen => {
|
||||||
if (CScreen) {
|
const vmScreen = new CScreen(),
|
||||||
const vmScreen = new CScreen(),
|
screenName = vmScreen.screenName;
|
||||||
screenName = vmScreen.screenName;
|
defaultScreenName || (defaultScreenName = screenName);
|
||||||
defaultScreenName || (defaultScreenName = screenName);
|
SCREENS.set(screenName, vmScreen);
|
||||||
SCREENS[screenName] = vmScreen;
|
});
|
||||||
|
|
||||||
|
SCREENS.forEach(vmScreen => {
|
||||||
|
if (!vmScreen.__started) {
|
||||||
|
vmScreen.onStart();
|
||||||
|
vmScreen.__started = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
forEachObjectValue(SCREENS, vmScreen => vmScreen.onStart());
|
|
||||||
|
|
||||||
const cross = new Crossroads();
|
const cross = new Crossroads();
|
||||||
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
|
cross.addRoute(/^([^/]*)\/?(.*)$/, screenOnRoute);
|
||||||
|
|
||||||
hasher.add(cross.parse.bind(cross));
|
hasher.add(cross.parse.bind(cross));
|
||||||
hasher.init();
|
hasher.init();
|
||||||
|
|
@ -325,21 +275,19 @@ export const
|
||||||
|
|
||||||
const c = elementById('rl-content'), l = elementById('rl-loading');
|
const c = elementById('rl-content'), l = elementById('rl-loading');
|
||||||
c && (c.hidden = false);
|
c && (c.hidden = false);
|
||||||
l && l.remove();
|
l?.remove();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by ko.bindingHandlers.command (template data-bind="command: ")
|
||||||
|
* to enable/disable click/submit action.
|
||||||
|
*/
|
||||||
decorateKoCommands = (thisArg, commands) =>
|
decorateKoCommands = (thisArg, commands) =>
|
||||||
forEachObjectEntry(commands, (key, canExecute) => {
|
forEachObjectEntry(commands, (key, canExecute) => {
|
||||||
let command = thisArg[key],
|
let command = thisArg[key],
|
||||||
fn = (...args) => fn.enabled() && fn.canExecute() && command.apply(thisArg, args);
|
fn = (...args) => fn.canExecute() && command.apply(thisArg, args);
|
||||||
|
|
||||||
// fn.__realCanExecute = canExecute;
|
fn.canExecute = koComputable(() => canExecute.call(thisArg, thisArg));
|
||||||
|
|
||||||
fn.enabled = ko.observable(true);
|
|
||||||
|
|
||||||
fn.canExecute = isFunction(canExecute)
|
|
||||||
? koComputable(() => fn.enabled() && canExecute.call(thisArg, thisArg))
|
|
||||||
: koComputable(() => fn.enabled());
|
|
||||||
|
|
||||||
thisArg[key] = fn;
|
thisArg[key] = fn;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
197
dev/Mime/Address.js
Normal file
197
dev/Mime/Address.js
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { decodeEncodedWords } from 'Mime/Encoding';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses structured e-mail addresses from an address/mailbox(-list) field
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* "Name <address@domain>"
|
||||||
|
*
|
||||||
|
* will be converted to
|
||||||
|
*
|
||||||
|
* [{name: "Name", email: "address@domain"}]
|
||||||
|
*
|
||||||
|
* @param {String} str Address field
|
||||||
|
* @return {Array} An array of address objects
|
||||||
|
*/
|
||||||
|
export function addressparser(str) {
|
||||||
|
str = (str || '').toString();
|
||||||
|
|
||||||
|
let
|
||||||
|
endOperator = '',
|
||||||
|
node = {
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
escaped = false,
|
||||||
|
address = [],
|
||||||
|
addresses = [];
|
||||||
|
|
||||||
|
const
|
||||||
|
/*
|
||||||
|
* Operator tokens and which tokens are expected to end the sequence
|
||||||
|
*/
|
||||||
|
OPERATORS = {
|
||||||
|
'"': '"',
|
||||||
|
'(': ')',
|
||||||
|
'<': '>',
|
||||||
|
',': '',
|
||||||
|
// Groups are ended by semicolons
|
||||||
|
':': ';',
|
||||||
|
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
||||||
|
// than for terminating a group, but they are also not valid for any
|
||||||
|
// other use in this context. Given that some mail clients have
|
||||||
|
// historically allowed the semicolon as a delimiter equivalent to the
|
||||||
|
// comma in their UI, it makes sense to treat them the same as a comma
|
||||||
|
// when used outside of a group.
|
||||||
|
';': ''
|
||||||
|
},
|
||||||
|
pushToken = token => {
|
||||||
|
token.value = (token.value || '').toString().trim();
|
||||||
|
token.value.length && address.push(token);
|
||||||
|
node = {
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
escaped = false;
|
||||||
|
},
|
||||||
|
pushAddress = () => {
|
||||||
|
if (address.length) {
|
||||||
|
address = _handleAddress(address);
|
||||||
|
if (address.length) {
|
||||||
|
addresses = addresses.concat(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
address = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
[...str].forEach(chr => {
|
||||||
|
if (!escaped && (chr === endOperator || (!endOperator && chr in OPERATORS))) {
|
||||||
|
pushToken(node);
|
||||||
|
if (',' === chr || ';' === chr) {
|
||||||
|
pushAddress();
|
||||||
|
} else {
|
||||||
|
endOperator = endOperator ? '' : OPERATORS[chr];
|
||||||
|
if ('<' === chr) {
|
||||||
|
node.type = 'email';
|
||||||
|
} else if ('(' === chr) {
|
||||||
|
node.type = 'comment';
|
||||||
|
} else if (':' === chr) {
|
||||||
|
node.type = 'group';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.value += chr;
|
||||||
|
escaped = !escaped && '\\' === chr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pushToken(node);
|
||||||
|
|
||||||
|
pushAddress();
|
||||||
|
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts tokens for a single address into an address object
|
||||||
|
*
|
||||||
|
* @param {Array} tokens Tokens object
|
||||||
|
* @return {Object} Address object
|
||||||
|
*/
|
||||||
|
function _handleAddress(tokens) {
|
||||||
|
let
|
||||||
|
isGroup = false,
|
||||||
|
address = {},
|
||||||
|
addresses = [],
|
||||||
|
data = {
|
||||||
|
email: [],
|
||||||
|
comment: [],
|
||||||
|
group: [],
|
||||||
|
text: []
|
||||||
|
};
|
||||||
|
|
||||||
|
tokens.forEach(token => {
|
||||||
|
isGroup = isGroup || 'group' === token.type;
|
||||||
|
data[token.type].push(token.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there is no text but a comment, replace the two
|
||||||
|
if (!data.text.length && data.comment.length) {
|
||||||
|
data.text = data.comment;
|
||||||
|
data.comment = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGroup) {
|
||||||
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
||||||
|
/*
|
||||||
|
addresses.push({
|
||||||
|
email: '',
|
||||||
|
name: data.text.join(' ').trim(),
|
||||||
|
group: addressparser(data.group.join(','))
|
||||||
|
// ,comment: data.comment.join(' ').trim()
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
addresses = addresses.concat(addressparser(data.group.join(',')));
|
||||||
|
} else {
|
||||||
|
// If no address was found, try to detect one from regular text
|
||||||
|
if (!data.email.length && data.text.length) {
|
||||||
|
var i = data.text.length;
|
||||||
|
while (i--) {
|
||||||
|
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
||||||
|
data.email = data.text.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// still no address
|
||||||
|
if (!data.email.length) {
|
||||||
|
i = data.text.length;
|
||||||
|
while (i--) {
|
||||||
|
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
|
||||||
|
if (!data.email.length) {
|
||||||
|
data.email = [address.trim()];
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return address.trim();
|
||||||
|
});
|
||||||
|
if (data.email.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's still no text but a comment exists, replace the two
|
||||||
|
if (!data.text.length && data.comment.length) {
|
||||||
|
data.text = data.comment;
|
||||||
|
data.comment = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only the first address occurence, push others to regular text
|
||||||
|
if (data.email.length > 1) {
|
||||||
|
data.text = data.text.concat(data.email.splice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
address = {
|
||||||
|
// Join values with spaces
|
||||||
|
email: decodeEncodedWords(data.email.join(' ').trim()),
|
||||||
|
name: decodeEncodedWords(data.text.join(' ').trim())
|
||||||
|
// ,comment: data.comment.join(' ').trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (address.email === address.name) {
|
||||||
|
if (address.email.includes('@')) {
|
||||||
|
address.name = '';
|
||||||
|
} else {
|
||||||
|
address.email = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
|
||||||
|
|
||||||
|
addresses.push(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
35
dev/Mime/Encoding.js
Normal file
35
dev/Mime/Encoding.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
const
|
||||||
|
QPDecodeParams = [/=([0-9A-F]{2})/g, (...args) => String.fromCharCode(parseInt(args[1], 16))];
|
||||||
|
|
||||||
|
export const
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
|
||||||
|
BDecode = atob,
|
||||||
|
|
||||||
|
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
||||||
|
BEncode = data => btoa(unescape(encodeURIComponent(data))),
|
||||||
|
/* // Without deprecated 'unescape':
|
||||||
|
BEncode = data => btoa(encodeURIComponent(data).replace(
|
||||||
|
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
||||||
|
)),
|
||||||
|
*/
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.7
|
||||||
|
QPDecode = data => data.replace(/=\r?\n/g, '').replace(...QPDecodeParams),
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2047#section-4.1
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2047#section-4.2
|
||||||
|
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
||||||
|
decodeEncodedWords = data =>
|
||||||
|
data.replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
|
||||||
|
decodeText(charset, 'B' == encoding ? BDecode(text) : QPDecode(text))
|
||||||
|
)
|
||||||
|
,
|
||||||
|
|
||||||
|
decodeText = (charset, data) => {
|
||||||
|
try {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
||||||
|
return new TextDecoder(charset).decode(Uint8Array.from(data, c => c.charCodeAt(0)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error({charset:charset,error:e});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,19 +1,75 @@
|
||||||
|
import { decodeEncodedWords, BDecode, BEncode, QPDecode, decodeText } from 'Mime/Encoding';
|
||||||
const
|
import { addressparser } from 'Mime/Address';
|
||||||
// RFC2045
|
|
||||||
QPDecodeIn = /=([0-9A-F]{2})/g,
|
|
||||||
QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
|
|
||||||
|
|
||||||
export function ParseMime(text)
|
export function ParseMime(text)
|
||||||
{
|
{
|
||||||
class MimePart
|
class MimePart
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
constructor() {
|
||||||
|
this.id = 0;
|
||||||
|
this.start = 0;
|
||||||
|
this.end = 0;
|
||||||
|
this.parts = [];
|
||||||
|
this.bodyStart = 0;
|
||||||
|
this.bodyEnd = 0;
|
||||||
|
this.boundary = '';
|
||||||
|
this.bodyText = '';
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4021
|
||||||
|
this.headers = {
|
||||||
|
// Required
|
||||||
|
date = null,
|
||||||
|
from = [], // mailbox-list
|
||||||
|
// Optional
|
||||||
|
sender = [], // mailbox MUST occur with multi-address
|
||||||
|
'reply-to' = [], // address-list
|
||||||
|
to = [], // address-list
|
||||||
|
cc = [], // address-list
|
||||||
|
bcc = [], // address-list
|
||||||
|
'message-id' = '', // msg-id SHOULD be present
|
||||||
|
'in-reply-to' = '', // 1*msg-id SHOULD occur in some replies
|
||||||
|
references = '', // 1*msg-id SHOULD occur in some replies
|
||||||
|
subject = '', // unstructured
|
||||||
|
// Optional unlimited
|
||||||
|
comments = [], // unstructured
|
||||||
|
keywords = [], // phrase *("," phrase)
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.6
|
||||||
|
'resent-date' = [],
|
||||||
|
'resent-from' = [],
|
||||||
|
'resent-sender' = [],
|
||||||
|
'resent-to' = [],
|
||||||
|
'resent-cc' = [],
|
||||||
|
'resent-bcc' = [],
|
||||||
|
'resent-msg-id' = [],
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.7
|
||||||
|
trace = [],
|
||||||
|
'return-path' = '', // angle-addr
|
||||||
|
received = [],
|
||||||
|
// optional others outside RFC2822
|
||||||
|
'mime-version' = '', // RFC2045
|
||||||
|
'content-transfer-encoding' = '',
|
||||||
|
'content-type' = '',
|
||||||
|
'delivered-to' = [], // RFC9228 addr-spec
|
||||||
|
'authentication-results' = '', // dkim, spf, dmarc
|
||||||
|
'dkim-signature' = '',
|
||||||
|
'x-rspamd-queue-id' = '',
|
||||||
|
'x-rspamd-action' = '',
|
||||||
|
'x-spamd-bar' = '',
|
||||||
|
'x-rspamd-server' = '',
|
||||||
|
'x-spamd-result' = '',
|
||||||
|
'x-remote-address' = '',
|
||||||
|
// etc.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
header(name) {
|
header(name) {
|
||||||
return this.headers && this.headers[name];
|
return this.headers?.[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
headerValue(name) {
|
headerValue(name) {
|
||||||
return (this.header(name) || {value:null}).value;
|
return this.header(name)?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
get raw() {
|
||||||
|
|
@ -26,88 +82,100 @@ export function ParseMime(text)
|
||||||
|
|
||||||
get body() {
|
get body() {
|
||||||
let body = this.bodyRaw,
|
let body = this.bodyRaw,
|
||||||
encoding = this.headerValue('content-transfer-encoding');
|
charset = this.header('content-type')?.params.charset,
|
||||||
|
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
body = QPDecode(body);
|
||||||
} else if ('base64' == encoding) {
|
} else if ('base64' == encoding) {
|
||||||
body = atob(body.replace(/\r?\n/g, ''));
|
body = BDecode(body.replace(/\r?\n/g, ''));
|
||||||
}
|
}
|
||||||
return body;
|
return decodeText(charset, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
get dataUrl() {
|
get dataUrl() {
|
||||||
let body = this.bodyRaw,
|
let body = this.bodyRaw,
|
||||||
encoding = this.headerValue('content-transfer-encoding');
|
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
|
||||||
if ('base64' == encoding) {
|
if ('base64' == encoding) {
|
||||||
body = body.replace(/\r?\n/g, '');
|
body = body.replace(/\r?\n/g, '');
|
||||||
} else {
|
} else {
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
body = QPDecode(body);
|
||||||
}
|
}
|
||||||
body = btoa(body);
|
body = BEncode(body);
|
||||||
}
|
}
|
||||||
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
||||||
}
|
}
|
||||||
|
|
||||||
forEach(fn) {
|
forEach(fn) {
|
||||||
fn(this);
|
fn(this);
|
||||||
if (this.parts) {
|
this.parts.forEach(part => part.forEach(fn));
|
||||||
this.parts.forEach(part => part.forEach(fn));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getByContentType(type) {
|
getByContentType(type) {
|
||||||
if (type == this.headerValue('content-type')) {
|
if (type == this.headerValue('content-type')?.toLowerCase()) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
if (this.parts) {
|
let i = 0, p = this.parts, part;
|
||||||
let i = 0, p = this.parts, part;
|
for (i; i < p.length; ++i) {
|
||||||
for (i; i < p.length; ++i) {
|
if ((part = p[i].getByContentType(type))) {
|
||||||
if ((part = p[i].getByContentType(type))) {
|
return part;
|
||||||
return part;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mailbox-list or address-list
|
||||||
|
const lists = ['from','reply-to','to','cc','bcc'];
|
||||||
|
|
||||||
const ParsePart = (mimePart, start_pos = 0, id = '') =>
|
const ParsePart = (mimePart, start_pos = 0, id = '') =>
|
||||||
{
|
{
|
||||||
let part = new MimePart;
|
let part = new MimePart,
|
||||||
|
head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/)?.[0],
|
||||||
|
headers = {};
|
||||||
if (id) {
|
if (id) {
|
||||||
part.id = id;
|
part.id = id;
|
||||||
part.start = start_pos;
|
part.start = start_pos;
|
||||||
part.end = start_pos + mimePart.length;
|
part.end = start_pos + mimePart.length;
|
||||||
}
|
}
|
||||||
|
part.parts = [];
|
||||||
|
|
||||||
// get headers
|
// get headers
|
||||||
let head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/);
|
|
||||||
if (head) {
|
if (head) {
|
||||||
head = head[0];
|
|
||||||
let headers = {};
|
|
||||||
head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => {
|
head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => {
|
||||||
let match = header.match(/^([^:]+):\s*([^;]+)/),
|
let match = header.match(/^([^:]+):\s*([^;]+)/),
|
||||||
params = {};
|
params = {};
|
||||||
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
if (match) {
|
||||||
params[param[1].trim().toLowerCase()] = param[2].trim()
|
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
||||||
);
|
params[param[1].trim().toLowerCase()] = param[2].trim()
|
||||||
headers[match[1].trim().toLowerCase()] = {
|
);
|
||||||
value: match[2].trim(),
|
let field = match[1].trim().toLowerCase();
|
||||||
params: params
|
if (lists.includes(field)) {
|
||||||
};
|
match[2] = addressparser(match[2]);
|
||||||
|
} else if ('keywords' === field) {
|
||||||
|
match[2] = match[2].split(',').forEach(entry => decodeEncodedWords(entry.trim()));
|
||||||
|
match[2] = (headers[field]?.value || []).concat(match[2]);
|
||||||
|
} else {
|
||||||
|
match[2] = decodeEncodedWords(match[2].trim());
|
||||||
|
if ('comments' === field) {
|
||||||
|
match[2] = (headers[field]?.value || []).push(match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers[field] = {
|
||||||
|
value: match[2],
|
||||||
|
params: params
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
part.headers = headers;
|
|
||||||
|
|
||||||
// get body
|
// get body
|
||||||
part.bodyStart = start_pos + head.length;
|
part.bodyStart = start_pos + head.length;
|
||||||
part.bodyEnd = start_pos + mimePart.length;
|
part.bodyEnd = start_pos + mimePart.length;
|
||||||
|
|
||||||
// get child parts
|
// get child parts
|
||||||
let boundary = headers['content-type'].params.boundary;
|
let boundary = headers['content-type']?.params.boundary;
|
||||||
if (boundary) {
|
if (boundary) {
|
||||||
part.boundary = boundary;
|
part.boundary = boundary;
|
||||||
part.parts = [];
|
let regex = new RegExp('(?:^|\r?\n)--' + RegExp.escape(boundary) + '(?:--)?(?:\r?\n|$)', 'g'),
|
||||||
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
|
|
||||||
body = mimePart.slice(head.length),
|
body = mimePart.slice(head.length),
|
||||||
bodies = body.split(regex),
|
bodies = body.split(regex),
|
||||||
pos = part.bodyStart;
|
pos = part.bodyStart;
|
||||||
|
|
@ -123,6 +191,8 @@ export function ParseMime(text)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
part.headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
return part;
|
return part;
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,40 @@ import { AttachmentModel } from 'Model/Attachment';
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
||||||
|
|
||||||
|
import { EmailModel } from 'Model/Email';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string data
|
* @param string data
|
||||||
* @param MessageModel message
|
* @param MessageModel message
|
||||||
*/
|
*/
|
||||||
export function MimeToMessage(data, message)
|
export function MimeToMessage(data, message)
|
||||||
{
|
{
|
||||||
let signed;
|
|
||||||
const struct = ParseMime(data);
|
const struct = ParseMime(data);
|
||||||
if (struct.headers) {
|
if (struct.headers) {
|
||||||
let html = struct.getByContentType('text/html');
|
let html = struct.getByContentType('text/html'),
|
||||||
|
subject = struct.headerValue('subject');
|
||||||
html = html ? html.body : '';
|
html = html ? html.body : '';
|
||||||
|
|
||||||
|
// Content-Type: ...; protected-headers="v1"
|
||||||
|
subject && message.subject(subject);
|
||||||
|
|
||||||
|
// EmailCollectionModel
|
||||||
|
['from','to'].forEach(name => {
|
||||||
|
const items = message[name];
|
||||||
|
struct.headerValue(name)?.forEach(item => {
|
||||||
|
item = new EmailModel(item.email, item.name);
|
||||||
|
// Make them unique
|
||||||
|
if (item.email && item.name || !items.find(address => address.email == item.email)) {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
struct.forEach(part => {
|
struct.forEach(part => {
|
||||||
let cd = part.header('content-disposition'),
|
let cd = part.header('content-disposition'),
|
||||||
cid = part.header('content-id'),
|
cId = part.header('content-id'),
|
||||||
type = part.header('content-type');
|
type = part.header('content-type');
|
||||||
if (cid || cd) {
|
if (cId || cd) {
|
||||||
// if (cd && 'attachment' === cd.value) {
|
// if (cd && 'attachment' === cd.value) {
|
||||||
let attachment = new AttachmentModel;
|
let attachment = new AttachmentModel;
|
||||||
attachment.mimeType = type.value;
|
attachment.mimeType = type.value;
|
||||||
|
|
@ -28,18 +45,15 @@ export function MimeToMessage(data, message)
|
||||||
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
|
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
|
||||||
attachment.fileType = FileInfo.getType('', type.value);
|
attachment.fileType = FileInfo.getType('', type.value);
|
||||||
attachment.url = part.dataUrl;
|
attachment.url = part.dataUrl;
|
||||||
attachment.friendlySize = FileInfo.friendlySize(part.body.length);
|
attachment.estimatedSize = part.body.length;
|
||||||
/*
|
/*
|
||||||
attachment.isThumbnail = false;
|
|
||||||
attachment.contentLocation = '';
|
attachment.contentLocation = '';
|
||||||
attachment.download = '';
|
|
||||||
attachment.folder = '';
|
attachment.folder = '';
|
||||||
attachment.uid = '';
|
attachment.uid = '';
|
||||||
attachment.mimeIndex = part.id;
|
attachment.mimeIndex = part.id;
|
||||||
attachment.framed = false;
|
|
||||||
*/
|
*/
|
||||||
attachment.cid = cid ? cid.value : '';
|
attachment.cId = cId ? cId.value : '';
|
||||||
if (cid && html) {
|
if (cId && html) {
|
||||||
let cid = 'cid:' + attachment.contentId(),
|
let cid = 'cid:' + attachment.contentId(),
|
||||||
found = html.includes(cid);
|
found = html.includes(cid);
|
||||||
attachment.isInline(found);
|
attachment.isInline(found);
|
||||||
|
|
@ -51,12 +65,28 @@ export function MimeToMessage(data, message)
|
||||||
} else {
|
} else {
|
||||||
message.attachments.push(attachment);
|
message.attachments.push(attachment);
|
||||||
}
|
}
|
||||||
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
|
} else if ('multipart/signed' === type.value) {
|
||||||
signed = {
|
let protocol = type.params.protocol;
|
||||||
MicAlg: type.micalg,
|
if ('application/pgp-signature' === protocol) {
|
||||||
BodyPart: part.parts[0],
|
message.pgpSigned({
|
||||||
SigPart: part.parts[1]
|
micAlg: type.micalg,
|
||||||
};
|
bodyPart: part.parts[0],
|
||||||
|
sigPart: part.parts[1]
|
||||||
|
});
|
||||||
|
} else if ('application/pkcs7-signature' === protocol.replace('x-')) {
|
||||||
|
message.smimeSigned({
|
||||||
|
micAlg: type.micalg,
|
||||||
|
bodyPart: part,
|
||||||
|
sigPart: part.parts[1], // For importing
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if ('application/pkcs7-mime' === type.value /*&& 'signed-data' === type.params['smime-type']=*/) {
|
||||||
|
message.smimeSigned({
|
||||||
|
micAlg: type.micalg,
|
||||||
|
bodyPart: part,
|
||||||
|
detached: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -67,10 +97,7 @@ export function MimeToMessage(data, message)
|
||||||
message.plain(data);
|
message.plain(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) {
|
if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
|
||||||
signed = true;
|
message.pgpSigned(true);
|
||||||
}
|
}
|
||||||
message.pgpSigned(signed);
|
|
||||||
|
|
||||||
// TODO: Verify instantly?
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ export class AbstractCollectionModel extends Array
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractCollectionModel) {
|
if (new.target === AbstractCollectionModel) {
|
||||||
throw new Error("Can't instantiate AbstractCollectionModel!");
|
throw Error("Can't instantiate AbstractCollectionModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy() {
|
onDestroy() {
|
||||||
this.forEach(item => item.onDestroy && item.onDestroy());
|
this.forEach(item => item.onDestroy?.());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -27,12 +27,10 @@ export class AbstractCollectionModel extends Array
|
||||||
forEachObjectEntry(json, (key, value) => '@' !== key[0] && (result[key] = value));
|
forEachObjectEntry(json, (key, value) => '@' !== key[0] && (result[key] = value));
|
||||||
json = json['@Collection'];
|
json = json['@Collection'];
|
||||||
}
|
}
|
||||||
if (isArray(json)) {
|
isArray(json) && json.forEach(item => {
|
||||||
json.forEach(item => {
|
item && itemCallback && (item = itemCallback(item, result));
|
||||||
item && itemCallback && (item = itemCallback(item, result));
|
item && result.push(item);
|
||||||
item && result.push(item);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
import { addObservablesTo } from 'External/ko';
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
export class AccountModel extends AbstractModel {
|
export class AccountModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
|
|
@ -6,16 +9,56 @@ export class AccountModel extends AbstractModel {
|
||||||
* @param {boolean=} canBeDelete = true
|
* @param {boolean=} canBeDelete = true
|
||||||
* @param {number=} count = 0
|
* @param {number=} count = 0
|
||||||
*/
|
*/
|
||||||
constructor(email/*, count = 0*/, isAdditional = true) {
|
constructor(email, name, isAdditional = true) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
|
|
||||||
this.addObservables({
|
this.displayName = name ? name + ' <' + email + '>' : email;
|
||||||
// count: count || 0,
|
|
||||||
|
addObservablesTo(this, {
|
||||||
|
unreadEmails: null,
|
||||||
askDelete: false,
|
askDelete: false,
|
||||||
isAdditional: isAdditional
|
isAdditional: isAdditional
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load at random between 3 and 30 seconds
|
||||||
|
SettingsUserStore.showUnreadCount() && isAdditional
|
||||||
|
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label() {
|
||||||
|
return this.name || IDN.toUnicode(this.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get INBOX unread messages
|
||||||
|
*/
|
||||||
|
fetchUnread() {
|
||||||
|
Remote.request('AccountUnread', (iError, oData) => {
|
||||||
|
iError || this.unreadEmails(oData?.Result?.unreadEmails || null);
|
||||||
|
}, {
|
||||||
|
email: this.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports all mail to main account
|
||||||
|
*//*
|
||||||
|
importAll(account) {
|
||||||
|
Remote.streamPerLine(line => {
|
||||||
|
try {
|
||||||
|
line = JSON.parse(line);
|
||||||
|
console.dir(line);
|
||||||
|
} catch (e) {
|
||||||
|
// OOPS
|
||||||
|
}
|
||||||
|
}, 'AccountImport', {
|
||||||
|
Action: 'AccountImport',
|
||||||
|
email: account.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { FileInfo, FileType } from 'Common/File';
|
import { FileInfo, FileType } from 'Common/File';
|
||||||
|
import { stopEvent, SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||||
|
import { b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import {
|
import {
|
||||||
attachmentDownload,
|
attachmentDownload,
|
||||||
serverRequestRaw
|
serverRequestRaw
|
||||||
} from 'Common/Links';
|
} from 'Common/Links';
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
import { addObservablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { SMAudio } from 'Common/Audio';
|
import { SMAudio } from 'Common/Audio';
|
||||||
|
|
||||||
|
|
@ -14,24 +17,22 @@ export class AttachmentModel extends AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.checked = ko.observable(false);
|
this.checked = ko.observable(true);
|
||||||
|
|
||||||
this.mimeType = '';
|
this.mimeType = '';
|
||||||
|
// this.mimeTypeParams = '';
|
||||||
this.fileName = '';
|
this.fileName = '';
|
||||||
this.fileNameExt = '';
|
this.fileNameExt = '';
|
||||||
this.fileType = FileType.Unknown;
|
this.fileType = FileType.Unknown;
|
||||||
this.friendlySize = '';
|
this.cId = '';
|
||||||
this.isThumbnail = false;
|
|
||||||
this.cid = '';
|
|
||||||
this.contentLocation = '';
|
this.contentLocation = '';
|
||||||
this.download = '';
|
|
||||||
this.folder = '';
|
this.folder = '';
|
||||||
this.uid = '';
|
this.uid = '';
|
||||||
this.url = '';
|
this.url = '';
|
||||||
this.mimeIndex = '';
|
this.mimeIndex = '';
|
||||||
this.framed = false;
|
this.estimatedSize = 0;
|
||||||
|
|
||||||
this.addObservables({
|
addObservablesTo(this, {
|
||||||
isInline: false,
|
isInline: false,
|
||||||
isLinked: false
|
isLinked: false
|
||||||
});
|
});
|
||||||
|
|
@ -45,16 +46,23 @@ export class AttachmentModel extends AbstractModel {
|
||||||
static reviveFromJson(json) {
|
static reviveFromJson(json) {
|
||||||
const attachment = super.reviveFromJson(json);
|
const attachment = super.reviveFromJson(json);
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
|
|
||||||
|
|
||||||
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
|
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
|
||||||
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
|
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
|
||||||
}
|
}
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleChecked(self, event) {
|
||||||
|
stopEvent(event);
|
||||||
|
self.checked(!self.checked());
|
||||||
|
}
|
||||||
|
|
||||||
|
friendlySize() {
|
||||||
|
return FileInfo.friendlySize(this.estimatedSize) + (this.isLinked() ? ' 🔗' : '');
|
||||||
|
}
|
||||||
|
|
||||||
contentId() {
|
contentId() {
|
||||||
return this.cid.replace(/^<+|>+$/g, '');
|
return this.cId.replace(/^<+|>+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -85,13 +93,6 @@ export class AttachmentModel extends AbstractModel {
|
||||||
return FileType.Audio === this.fileType && 'wav' === this.fileNameExt;
|
return FileType.Audio === this.fileType && 'wav' === this.fileNameExt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
hasThumbnail() {
|
|
||||||
return this.isThumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
|
|
@ -124,6 +125,20 @@ export class AttachmentModel extends AbstractModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get download() {
|
||||||
|
return b64EncodeJSONSafe(this.url ? {
|
||||||
|
fileName: this.fileName,
|
||||||
|
data: this.url.replace(/^.+,/, '')
|
||||||
|
} : {
|
||||||
|
folder: this.folder,
|
||||||
|
uid: this.uid,
|
||||||
|
mimeIndex: this.mimeIndex,
|
||||||
|
mimeType: this.mimeType,
|
||||||
|
fileName: this.fileName,
|
||||||
|
accountHash: SettingsGet('accountHash')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -138,11 +153,20 @@ export class AttachmentModel extends AbstractModel {
|
||||||
return this.url || serverRequestRaw('View', this.download);
|
return this.url || serverRequestRaw('View', this.download);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
hasThumbnail() {
|
||||||
|
return SettingsCapa('AttachmentThumbnails') && this.isImage() && !this.isLinked();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
linkThumbnailPreviewStyle() {
|
thumbnailStyle() {
|
||||||
return this.hasThumbnail() ? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')' : '';
|
return this.hasThumbnail()
|
||||||
|
? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')'
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -173,7 +197,7 @@ export class AttachmentModel extends AbstractModel {
|
||||||
const localEvent = event.originalEvent || event;
|
const localEvent = event.originalEvent || event;
|
||||||
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
|
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
|
||||||
let link = this.linkDownload();
|
let link = this.linkDownload();
|
||||||
if ('http' !== link.slice(0, 4)) {
|
if (!link.startsWith('http')) {
|
||||||
link = location.protocol + '//' + location.host + location.pathname + link;
|
link = location.protocol + '//' + location.host + location.pathname + link;
|
||||||
}
|
}
|
||||||
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);
|
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { baseCollator } from 'Common/Translator';
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { AttachmentModel } from 'Model/Attachment';
|
import { AttachmentModel } from 'Model/Attachment';
|
||||||
|
|
||||||
|
|
@ -10,29 +11,32 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
||||||
* @returns {AttachmentCollectionModel}
|
* @returns {AttachmentCollectionModel}
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(items) {
|
static reviveFromJson(items) {
|
||||||
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
|
||||||
/*
|
|
||||||
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
||||||
|
let collator = baseCollator(true);
|
||||||
|
attachments.sort((a, b) => {
|
||||||
|
if (a.isInline()) {
|
||||||
|
if (!b.isInline()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else if (!b.isInline()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return collator.compare(a.fileName, b.fileName);
|
||||||
|
});
|
||||||
|
/*
|
||||||
if (attachments) {
|
if (attachments) {
|
||||||
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
|
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
|
||||||
}
|
}
|
||||||
return attachments;
|
|
||||||
*/
|
*/
|
||||||
|
return attachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @param {string} cId
|
||||||
*/
|
|
||||||
hasVisible() {
|
|
||||||
return !!this.filter(item => !item.isLinked()).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} cid
|
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
findByCid(cid) {
|
findByCid(cId) {
|
||||||
cid = cid.replace(/^<+|>+$/g, '');
|
cId = cId.replace(/^<+|>+$/g, '');
|
||||||
return this.find(item => cid === item.contentId());
|
return this.find(item => cId === item.contentId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { pInt } from 'Common/Utils';
|
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
export class ComposeAttachmentModel extends AbstractModel {
|
export class ComposeAttachmentModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,23 +10,24 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
* @param {?number=} size = null
|
* @param {?number=} size = null
|
||||||
* @param {boolean=} isInline = false
|
* @param {boolean=} isInline = false
|
||||||
* @param {boolean=} isLinked = false
|
* @param {boolean=} isLinked = false
|
||||||
* @param {string=} CID = ''
|
* @param {string=} cId = ''
|
||||||
* @param {string=} contentLocation = ''
|
* @param {string=} contentLocation = ''
|
||||||
*/
|
*/
|
||||||
constructor(id, fileName, size = null, isInline = false, isLinked = false, CID = '', contentLocation = '') {
|
constructor(id, fileName, size = null, isInline = false, isLinked = false, cId = '', contentLocation = '') {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.isInline = !!isInline;
|
this.isInline = !!isInline;
|
||||||
this.isLinked = !!isLinked;
|
this.isLinked = !!isLinked;
|
||||||
this.CID = CID;
|
this.cId = cId;
|
||||||
this.contentLocation = contentLocation;
|
this.contentLocation = contentLocation;
|
||||||
this.fromMessage = false;
|
this.fromMessage = false;
|
||||||
|
|
||||||
this.addObservables({
|
addObservablesTo(this, {
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
size: size,
|
size: size,
|
||||||
tempName: '',
|
tempName: '',
|
||||||
|
type: '', // application/octet-stream
|
||||||
|
|
||||||
progress: 0,
|
progress: 0,
|
||||||
error: '',
|
error: '',
|
||||||
|
|
@ -36,15 +37,15 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
complete: false
|
complete: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addComputables({
|
addComputablesTo(this, {
|
||||||
progressText: () => {
|
progressText: () => {
|
||||||
const p = this.progress();
|
const p = this.progress();
|
||||||
return 0 === p ? '' : '' + (98 < p ? 100 : p) + '%';
|
return 1 > p ? '' : (100 < p ? 100 : p) + '%';
|
||||||
},
|
},
|
||||||
|
|
||||||
progressStyle: () => {
|
progressStyle: () => {
|
||||||
const p = this.progress();
|
const p = this.progress();
|
||||||
return 0 === p ? '' : 'width:' + (98 < p ? 100 : p) + '%';
|
return 1 > p ? '' : 'width:' + (100 < p ? 100 : p) + '%';
|
||||||
},
|
},
|
||||||
|
|
||||||
title: () => this.error() || this.fileName(),
|
title: () => this.error() || this.fileName(),
|
||||||
|
|
@ -54,48 +55,10 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
return null === localSize ? '' : FileInfo.friendlySize(localSize);
|
return null === localSize ? '' : FileInfo.friendlySize(localSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
mimeType: () => FileInfo.getContentType(this.fileName()),
|
mimeType: () => this.type() || FileInfo.getContentType(this.fileName()),
|
||||||
fileExt: () => FileInfo.getExtension(this.fileName())
|
fileExt: () => FileInfo.getExtension(this.fileName()),
|
||||||
|
|
||||||
|
iconClass: () => FileInfo.getIconClass(this.fileExt(), this.mimeType())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromAttachment(item)
|
|
||||||
{
|
|
||||||
const attachment = new ComposeAttachmentModel(
|
|
||||||
item.download,
|
|
||||||
item.fileName,
|
|
||||||
item.estimatedSize,
|
|
||||||
item.isInline(),
|
|
||||||
item.isLinked(),
|
|
||||||
item.cid,
|
|
||||||
item.contentLocation
|
|
||||||
);
|
|
||||||
attachment.fromMessage = true;
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {FetchJsonComposeAttachment} json
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
initByUploadJson(json) {
|
|
||||||
let bResult = false;
|
|
||||||
if (json) {
|
|
||||||
this.fileName(json.Name);
|
|
||||||
this.size(undefined === json.Size ? 0 : pInt(json.Size));
|
|
||||||
this.tempName(undefined === json.TempName ? '' : json.TempName);
|
|
||||||
this.isInline = false;
|
|
||||||
|
|
||||||
bResult = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
iconClass() {
|
|
||||||
return FileInfo.getIconClass(this.fileExt(), this.mimeType());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue