mirror of
https://github.com/axllent/mailpit.git
synced 2025-01-22 03:39:59 +02:00
Merge branch 'release/1.2.0'
This commit is contained in:
commit
d253d3164e
@ -1,31 +0,0 @@
|
||||
7c73d1883ba2ee2a9fe39d7c1d90e19d9606b0a1 commit refs/heads/develop
|
||||
d6fb872c795a8d57f6813da55d6cf3b453a96cc7 commit refs/heads/feature/version
|
||||
89e6fb4c42d70fa5a44075a2023cc6b520c17d8c commit refs/heads/master
|
||||
7c73d1883ba2ee2a9fe39d7c1d90e19d9606b0a1 commit refs/remotes/origin/develop
|
||||
89e6fb4c42d70fa5a44075a2023cc6b520c17d8c commit refs/remotes/origin/master
|
||||
438504fa46b2aa6847096d45be682c92bcfa00a2 commit refs/stash
|
||||
f0c66b466e49942d7ff47a4a788ba41d6028ae8b tag refs/tags/0.0.1-beta
|
||||
6c2be2ac88682884897fc440805c03679c2ced84 tag refs/tags/0.0.2
|
||||
1f39040d1d6e5c7804a090ae5cd09f2e0fdaca09 tag refs/tags/0.0.3
|
||||
3b781c77672a03600b170426298c0b30c0433542 tag refs/tags/0.0.4
|
||||
82f78fbf5a75d5dd3dae03e7c7ecedce99fc4642 tag refs/tags/0.0.5
|
||||
df9b109f382ff148c4254a4c98b0511c09f6687b tag refs/tags/0.0.6
|
||||
52859e22f355fafb6e1babf3d83e6e84e2aa380d tag refs/tags/0.0.7
|
||||
5e3e91a22c3557b3af1f787bd5b8a1ee462f5604 tag refs/tags/0.0.8
|
||||
0eb4066a4ce7839b81b6333be0e9660c8811b913 tag refs/tags/0.0.9
|
||||
13d94ea2541ad13d2f12825c604467b11f3e4a8a tag refs/tags/0.1.0
|
||||
e591d5c093ee3ba06a0c1ee891f961e287faeeb3 tag refs/tags/0.1.1
|
||||
4f14cdea00bf0fd4299e557cf3e895b548a0f982 tag refs/tags/0.1.2
|
||||
2be1a43f3990adb865c9e1a713c168cba9d01af7 tag refs/tags/0.1.3
|
||||
625b72b1eead52df071716d95d49aed28ae59d64 tag refs/tags/0.1.4
|
||||
39e6757bea81d8c8b52d3765cb198c4d70a200bb tag refs/tags/0.1.5
|
||||
ddf198cb39f4ed254558819ece53af6ec2f0d568 tag refs/tags/1.0.0
|
||||
60079cf37f9e53c93853bfe99094fde764feec5c tag refs/tags/1.0.0-beta1
|
||||
15371a9cd3671238811c7467f86decc3c00b7119 tag refs/tags/1.1.0
|
||||
88361be8a0fe56b147b44ea5fa5e754d2461e530 tag refs/tags/1.1.1
|
||||
9c9bc84baa24de70658bf402ebdae32c861dd3b1 tag refs/tags/1.1.2
|
||||
ee82b104198a75f3d837b7ab678c2a427e399a47 tag refs/tags/1.1.3
|
||||
28cde017eb7f904b77c00265cde6f3fe319af983 tag refs/tags/1.1.4
|
||||
6a008ae9c9fd101721b399e75f562dc9240f4372 tag refs/tags/1.1.5
|
||||
f3811c3be7c7aad71b03ece17fe4412890bc0ea1 tag refs/tags/1.1.6
|
||||
4da0e28ac47593b415c3e35aa13ff9cfb2be904d tag refs/tags/1.1.7
|
@ -1,6 +0,0 @@
|
||||
tree 1f6b4426212043bf6b473fcbac898e9b871c84f6
|
||||
parent 802f6f5672200fff036a72abd84f39e78483d61f
|
||||
author Ralph Slooten <axllent@gmail.com> 1659602885 +1200
|
||||
committer Ralph Slooten <axllent@gmail.com> 1659602885 +1200
|
||||
|
||||
Release 0.0.6
|
@ -1 +0,0 @@
|
||||
refs/heads/develop
|
@ -1 +0,0 @@
|
||||
00d254d7c41be358cc0a99d388e796703c3ee5b9
|
@ -1 +0,0 @@
|
||||
07141d3a213a443b830fb05dae87f181a6b213cc
|
@ -1 +0,0 @@
|
||||
0799a6d67cfd65d0228014478632f85780ed5517
|
@ -1 +0,0 @@
|
||||
0fa58dffbd9d30f4c0aa9c93c96292fa89857bc5
|
@ -1 +0,0 @@
|
||||
154b2342056d3adb1be716e16f8919319d3c6dba
|
@ -1 +0,0 @@
|
||||
19966fad81e043ef9e2a2d062411e3990dad449d
|
@ -1 +0,0 @@
|
||||
1df270bab3caf8c2e36c5d6aaf84efdfb5da0db0
|
@ -1 +0,0 @@
|
||||
2944c2a32f6e1e3cbb7eb9603188b339f6d74700
|
@ -1 +0,0 @@
|
||||
2e5752f69385f8d3f75ab1446e46d3f2208a543f
|
@ -1 +0,0 @@
|
||||
3103b50f0857d18c48ec60ee622fabf4938c938f
|
@ -1 +0,0 @@
|
||||
324de3d99f22bbce918e0246daa6caf90a0fc734
|
@ -1 +0,0 @@
|
||||
335b0f3876e66da7bb06e6c39afb016068da1ac9
|
@ -1 +0,0 @@
|
||||
38da162cd90445a64e6586f72fed0a3c5e7dfce2
|
@ -1 +0,0 @@
|
||||
3bbc12286966b69455025aa4de3e88ecda439811
|
@ -1 +0,0 @@
|
||||
41c7c2a93af86acb48e5dd8b3d9406820c547b13
|
@ -1 +0,0 @@
|
||||
48db1437b3a9ea21bc987513c01bc4800b06f0f9
|
@ -1 +0,0 @@
|
||||
493e8a09aa10d8fe48896c429ea0c1260b2c0709
|
@ -1 +0,0 @@
|
||||
4b707537b9334641c37fb1d1aab96fe708e507ec
|
@ -1 +0,0 @@
|
||||
4e4fc22cb53abe2979d751e63671c771718147e5
|
@ -1 +0,0 @@
|
||||
50c7955e6d3b39b187c7aaa7da1cf901be6ac6b2
|
@ -1 +0,0 @@
|
||||
55fd56a4a3e40acbfa4720749018d13873d8af62
|
@ -1 +0,0 @@
|
||||
56449dd30e9f26312ddb2117faf319a514f73f1f
|
@ -1 +0,0 @@
|
||||
6d426c2a0bcd5c191c0e95163cf15b69a207fdd5
|
@ -1 +0,0 @@
|
||||
6fe1bdb579f0924b10b28f1fc9587ac49fc44f24
|
@ -1 +0,0 @@
|
||||
72709acb903556967dd5d1cead1f5b052151bb25
|
@ -1 +0,0 @@
|
||||
76c6c881d54b82d5ca4bc427c2968fbc15c7a090
|
@ -1 +0,0 @@
|
||||
7a9b11a9e5e63b987fde0e7d8536f9fa76585097
|
@ -1 +0,0 @@
|
||||
7cbe3a04ef7b95789287cf8fa7d654e18df11084
|
@ -1 +0,0 @@
|
||||
7fd73a6fdb49843d29aefa2bb8bb5de900e53811
|
@ -1 +0,0 @@
|
||||
8019d3e0e2a8a01f729911730128de72a2a1b6ba
|
@ -1 +0,0 @@
|
||||
802f6f5672200fff036a72abd84f39e78483d61f
|
@ -1 +0,0 @@
|
||||
8159694b935775e4072e1902124a8cb4bb6efacd
|
@ -1 +0,0 @@
|
||||
83f289eb40b34c07ad6b5df1099a7b6427e9be40
|
@ -1 +0,0 @@
|
||||
86d73f9118eee40fd842fc25898490a2923eaecd
|
@ -1 +0,0 @@
|
||||
8866720631a717b8df2df6fc206eb6db44f8a005
|
@ -1 +0,0 @@
|
||||
8d308a6776436ce1189e45268e4eceb8b5a1011a
|
@ -1 +0,0 @@
|
||||
8d6d48c59e9ca78d00a12c715494de99eb40a5ad
|
@ -1 +0,0 @@
|
||||
8e819aca56db7ab982243eddda0ab89525db1b23
|
@ -1 +0,0 @@
|
||||
8f474bc31303882270112cdfafeaedc908938707
|
@ -1 +0,0 @@
|
||||
927638f2d500dbfd29371b2b4a3f01b987749899
|
@ -1 +0,0 @@
|
||||
9a27f330791c08ee87dbb886c6d511bb6d3bffa0
|
@ -1 +0,0 @@
|
||||
a3b34ec32effa03acfa4aa99cbe38aee81f01e56
|
@ -1 +0,0 @@
|
||||
a56c9616adbf6f5c387a2ffc472cd42b6b9ba377
|
@ -1 +0,0 @@
|
||||
a810bdae244404a91d17c6293f3096b055dd74cf
|
@ -1 +0,0 @@
|
||||
a85a74bb9a03dafeaaaf3e2211cb796f195773e3
|
@ -1 +0,0 @@
|
||||
ad1037c02b654b7e582c2c644841ea7477fcf9b4
|
@ -1 +0,0 @@
|
||||
b7dbda0db65e24c8ad2549639cd22388db837e1f
|
@ -1 +0,0 @@
|
||||
bca7bec867cd6aff1b1692ed5508c18e62aa430b
|
@ -1 +0,0 @@
|
||||
bd87dcabf6b370316eac39b863ee5d84ad5851e2
|
@ -1 +0,0 @@
|
||||
c6f1c8213b0bb6489072bc379ae8b158e6a552d9
|
@ -1 +0,0 @@
|
||||
ce23e0616e4a29a266bbfc9c398c57c2976dc71a
|
@ -1 +0,0 @@
|
||||
cf12b3968b2d2a59fb1fa12ef83065b1d7090681
|
@ -1 +0,0 @@
|
||||
cf5c4297c9b975dff541a7384da6838dd7fab930
|
@ -1 +0,0 @@
|
||||
d15b3eb05e53326a81eab3f6839186e0840cb686
|
@ -1 +0,0 @@
|
||||
d4f17fb8ad988be6cbd467ff2d79a5ac894c5e89
|
@ -1 +0,0 @@
|
||||
e1dc184d7b25a64cc9b599b38cafbc3e7bfccb76
|
@ -1 +0,0 @@
|
||||
e363ece5a05eeb55264176e689ffdbf4e79b718f
|
@ -1 +0,0 @@
|
||||
eb7049163bbfa1bc73af5859dff1803901cf43b3
|
@ -1 +0,0 @@
|
||||
f6da1c6da4beacaa410beea8dc1fe45d21b3b8d2
|
@ -1 +0,0 @@
|
||||
f966b6a756f736f3f106a186faac183ce0eb0686
|
@ -1 +0,0 @@
|
||||
Release 0.0.6
|
@ -1 +0,0 @@
|
||||
refs/heads/develop
|
@ -1,212 +0,0 @@
|
||||
7a9b11a9e5e63b987fde0e7d8536f9fa76585097
|
||||
6d426c2a0bcd5c191c0e95163cf15b69a207fdd5 7a9b11a9e5e63b987fde0e7d8536f9fa76585097
|
||||
f6da1c6da4beacaa410beea8dc1fe45d21b3b8d2 6d426c2a0bcd5c191c0e95163cf15b69a207fdd5
|
||||
76c6c881d54b82d5ca4bc427c2968fbc15c7a090 7a9b11a9e5e63b987fde0e7d8536f9fa76585097 f6da1c6da4beacaa410beea8dc1fe45d21b3b8d2
|
||||
8159694b935775e4072e1902124a8cb4bb6efacd f6da1c6da4beacaa410beea8dc1fe45d21b3b8d2 76c6c881d54b82d5ca4bc427c2968fbc15c7a090
|
||||
07141d3a213a443b830fb05dae87f181a6b213cc 8159694b935775e4072e1902124a8cb4bb6efacd
|
||||
cf5c4297c9b975dff541a7384da6838dd7fab930 76c6c881d54b82d5ca4bc427c2968fbc15c7a090 07141d3a213a443b830fb05dae87f181a6b213cc
|
||||
eb7049163bbfa1bc73af5859dff1803901cf43b3 07141d3a213a443b830fb05dae87f181a6b213cc cf5c4297c9b975dff541a7384da6838dd7fab930
|
||||
927638f2d500dbfd29371b2b4a3f01b987749899 eb7049163bbfa1bc73af5859dff1803901cf43b3
|
||||
50c7955e6d3b39b187c7aaa7da1cf901be6ac6b2 cf5c4297c9b975dff541a7384da6838dd7fab930 927638f2d500dbfd29371b2b4a3f01b987749899
|
||||
e1dc184d7b25a64cc9b599b38cafbc3e7bfccb76 927638f2d500dbfd29371b2b4a3f01b987749899 50c7955e6d3b39b187c7aaa7da1cf901be6ac6b2
|
||||
a56c9616adbf6f5c387a2ffc472cd42b6b9ba377 e1dc184d7b25a64cc9b599b38cafbc3e7bfccb76
|
||||
a3b34ec32effa03acfa4aa99cbe38aee81f01e56 50c7955e6d3b39b187c7aaa7da1cf901be6ac6b2 a56c9616adbf6f5c387a2ffc472cd42b6b9ba377
|
||||
d4f17fb8ad988be6cbd467ff2d79a5ac894c5e89 a56c9616adbf6f5c387a2ffc472cd42b6b9ba377 a3b34ec32effa03acfa4aa99cbe38aee81f01e56
|
||||
b7dbda0db65e24c8ad2549639cd22388db837e1f d4f17fb8ad988be6cbd467ff2d79a5ac894c5e89
|
||||
0799a6d67cfd65d0228014478632f85780ed5517 b7dbda0db65e24c8ad2549639cd22388db837e1f
|
||||
493e8a09aa10d8fe48896c429ea0c1260b2c0709 a3b34ec32effa03acfa4aa99cbe38aee81f01e56 0799a6d67cfd65d0228014478632f85780ed5517
|
||||
0fa58dffbd9d30f4c0aa9c93c96292fa89857bc5 0799a6d67cfd65d0228014478632f85780ed5517 493e8a09aa10d8fe48896c429ea0c1260b2c0709
|
||||
cf12b3968b2d2a59fb1fa12ef83065b1d7090681 0fa58dffbd9d30f4c0aa9c93c96292fa89857bc5
|
||||
2e5752f69385f8d3f75ab1446e46d3f2208a543f cf12b3968b2d2a59fb1fa12ef83065b1d7090681
|
||||
4e4fc22cb53abe2979d751e63671c771718147e5 2e5752f69385f8d3f75ab1446e46d3f2208a543f
|
||||
8e819aca56db7ab982243eddda0ab89525db1b23 4e4fc22cb53abe2979d751e63671c771718147e5
|
||||
8d6d48c59e9ca78d00a12c715494de99eb40a5ad 8e819aca56db7ab982243eddda0ab89525db1b23
|
||||
324de3d99f22bbce918e0246daa6caf90a0fc734 8d6d48c59e9ca78d00a12c715494de99eb40a5ad
|
||||
335b0f3876e66da7bb06e6c39afb016068da1ac9 324de3d99f22bbce918e0246daa6caf90a0fc734
|
||||
a85a74bb9a03dafeaaaf3e2211cb796f195773e3 335b0f3876e66da7bb06e6c39afb016068da1ac9
|
||||
ce23e0616e4a29a266bbfc9c398c57c2976dc71a 8d6d48c59e9ca78d00a12c715494de99eb40a5ad a85a74bb9a03dafeaaaf3e2211cb796f195773e3
|
||||
7cbe3a04ef7b95789287cf8fa7d654e18df11084 ce23e0616e4a29a266bbfc9c398c57c2976dc71a
|
||||
f966b6a756f736f3f106a186faac183ce0eb0686 493e8a09aa10d8fe48896c429ea0c1260b2c0709 7cbe3a04ef7b95789287cf8fa7d654e18df11084
|
||||
a810bdae244404a91d17c6293f3096b055dd74cf 7cbe3a04ef7b95789287cf8fa7d654e18df11084 f966b6a756f736f3f106a186faac183ce0eb0686
|
||||
56449dd30e9f26312ddb2117faf319a514f73f1f a810bdae244404a91d17c6293f3096b055dd74cf
|
||||
38da162cd90445a64e6586f72fed0a3c5e7dfce2 56449dd30e9f26312ddb2117faf319a514f73f1f
|
||||
c6f1c8213b0bb6489072bc379ae8b158e6a552d9 f966b6a756f736f3f106a186faac183ce0eb0686 38da162cd90445a64e6586f72fed0a3c5e7dfce2
|
||||
55fd56a4a3e40acbfa4720749018d13873d8af62 38da162cd90445a64e6586f72fed0a3c5e7dfce2 c6f1c8213b0bb6489072bc379ae8b158e6a552d9
|
||||
3bbc12286966b69455025aa4de3e88ecda439811 55fd56a4a3e40acbfa4720749018d13873d8af62
|
||||
7fd73a6fdb49843d29aefa2bb8bb5de900e53811 3bbc12286966b69455025aa4de3e88ecda439811
|
||||
83f289eb40b34c07ad6b5df1099a7b6427e9be40 7fd73a6fdb49843d29aefa2bb8bb5de900e53811
|
||||
72709acb903556967dd5d1cead1f5b052151bb25 83f289eb40b34c07ad6b5df1099a7b6427e9be40
|
||||
d15b3eb05e53326a81eab3f6839186e0840cb686 72709acb903556967dd5d1cead1f5b052151bb25
|
||||
bca7bec867cd6aff1b1692ed5508c18e62aa430b d15b3eb05e53326a81eab3f6839186e0840cb686
|
||||
4b707537b9334641c37fb1d1aab96fe708e507ec 7cbe3a04ef7b95789287cf8fa7d654e18df11084
|
||||
ad1037c02b654b7e582c2c644841ea7477fcf9b4 4b707537b9334641c37fb1d1aab96fe708e507ec
|
||||
154b2342056d3adb1be716e16f8919319d3c6dba ad1037c02b654b7e582c2c644841ea7477fcf9b4
|
||||
41c7c2a93af86acb48e5dd8b3d9406820c547b13 154b2342056d3adb1be716e16f8919319d3c6dba
|
||||
2944c2a32f6e1e3cbb7eb9603188b339f6d74700 41c7c2a93af86acb48e5dd8b3d9406820c547b13
|
||||
00d254d7c41be358cc0a99d388e796703c3ee5b9 2944c2a32f6e1e3cbb7eb9603188b339f6d74700
|
||||
8d308a6776436ce1189e45268e4eceb8b5a1011a 00d254d7c41be358cc0a99d388e796703c3ee5b9
|
||||
3103b50f0857d18c48ec60ee622fabf4938c938f 8d308a6776436ce1189e45268e4eceb8b5a1011a
|
||||
8f474bc31303882270112cdfafeaedc908938707 3103b50f0857d18c48ec60ee622fabf4938c938f
|
||||
8866720631a717b8df2df6fc206eb6db44f8a005 bca7bec867cd6aff1b1692ed5508c18e62aa430b 8f474bc31303882270112cdfafeaedc908938707
|
||||
8019d3e0e2a8a01f729911730128de72a2a1b6ba 8866720631a717b8df2df6fc206eb6db44f8a005
|
||||
bd87dcabf6b370316eac39b863ee5d84ad5851e2 8019d3e0e2a8a01f729911730128de72a2a1b6ba
|
||||
86d73f9118eee40fd842fc25898490a2923eaecd bd87dcabf6b370316eac39b863ee5d84ad5851e2
|
||||
e363ece5a05eeb55264176e689ffdbf4e79b718f c6f1c8213b0bb6489072bc379ae8b158e6a552d9 86d73f9118eee40fd842fc25898490a2923eaecd
|
||||
9a27f330791c08ee87dbb886c6d511bb6d3bffa0 bd87dcabf6b370316eac39b863ee5d84ad5851e2 e363ece5a05eeb55264176e689ffdbf4e79b718f
|
||||
6fe1bdb579f0924b10b28f1fc9587ac49fc44f24 9a27f330791c08ee87dbb886c6d511bb6d3bffa0
|
||||
1df270bab3caf8c2e36c5d6aaf84efdfb5da0db0 6fe1bdb579f0924b10b28f1fc9587ac49fc44f24
|
||||
48db1437b3a9ea21bc987513c01bc4800b06f0f9 e363ece5a05eeb55264176e689ffdbf4e79b718f 1df270bab3caf8c2e36c5d6aaf84efdfb5da0db0
|
||||
19966fad81e043ef9e2a2d062411e3990dad449d 6fe1bdb579f0924b10b28f1fc9587ac49fc44f24 48db1437b3a9ea21bc987513c01bc4800b06f0f9
|
||||
802f6f5672200fff036a72abd84f39e78483d61f 19966fad81e043ef9e2a2d062411e3990dad449d
|
||||
f74bb70499424355b8ca1a0e1338131b0ecabd34 802f6f5672200fff036a72abd84f39e78483d61f
|
||||
9d257dd3c03a3d07323d5f74f6d94bc7f02f4de7 48db1437b3a9ea21bc987513c01bc4800b06f0f9 f74bb70499424355b8ca1a0e1338131b0ecabd34
|
||||
f807c166f7265440c9a190d05f67904d99812f65 802f6f5672200fff036a72abd84f39e78483d61f 9d257dd3c03a3d07323d5f74f6d94bc7f02f4de7
|
||||
9fed08245a87d631d1ee4f9d75a60dc1d8940a3e f807c166f7265440c9a190d05f67904d99812f65
|
||||
123b0f19dbcd0542a73df6f487372ace03bfc5ff 19966fad81e043ef9e2a2d062411e3990dad449d
|
||||
4b9b60f2476275e3cf4dab64d61d8512318c4605 9fed08245a87d631d1ee4f9d75a60dc1d8940a3e 123b0f19dbcd0542a73df6f487372ace03bfc5ff
|
||||
47376d4db98f6b303e05240bbd154085442cb913 4b9b60f2476275e3cf4dab64d61d8512318c4605
|
||||
74fe6d55b4ed096bb3b3a3a497389312e6f3adcc 47376d4db98f6b303e05240bbd154085442cb913
|
||||
fc8148bfb33835495499227d9b808d0572c0b7ee 9d257dd3c03a3d07323d5f74f6d94bc7f02f4de7 74fe6d55b4ed096bb3b3a3a497389312e6f3adcc
|
||||
e0f7d88d6138e849aee9aa454b32206c0f147ae3 47376d4db98f6b303e05240bbd154085442cb913 fc8148bfb33835495499227d9b808d0572c0b7ee
|
||||
f7502b1c143672d92b32f24974fc95f2ae4111b9 e0f7d88d6138e849aee9aa454b32206c0f147ae3
|
||||
970a534d77b0ef4c9ea3ff201f5e6281b1bacef2 f7502b1c143672d92b32f24974fc95f2ae4111b9
|
||||
3b65a8852e2dfd64130e1bf4b4e625d55813c62c 970a534d77b0ef4c9ea3ff201f5e6281b1bacef2
|
||||
cbe61e3f2e68d4c3b607ff8ad2cfb8dabbb11a3a 3b65a8852e2dfd64130e1bf4b4e625d55813c62c
|
||||
54d3f6e3adee9d4c4cb8f668d2cd1dfa5e28029e cbe61e3f2e68d4c3b607ff8ad2cfb8dabbb11a3a
|
||||
22a476ded520ce7052e3960e546ac1136fada0e8 54d3f6e3adee9d4c4cb8f668d2cd1dfa5e28029e
|
||||
9fc7202552ced5424f01fb2815e47d8ae98131d2 fc8148bfb33835495499227d9b808d0572c0b7ee 22a476ded520ce7052e3960e546ac1136fada0e8
|
||||
4f266cd3f3d9a0592928496f37d7c4793ab0ecd1 54d3f6e3adee9d4c4cb8f668d2cd1dfa5e28029e 9fc7202552ced5424f01fb2815e47d8ae98131d2
|
||||
2d221a6b67c39c0f96f2f864e9edc5f91fed3616 4f266cd3f3d9a0592928496f37d7c4793ab0ecd1
|
||||
ad49bf28982b20db3d541313180ef22337574d57 2d221a6b67c39c0f96f2f864e9edc5f91fed3616
|
||||
58601710028dd8d17eb837df915f1b38826b1fca ad49bf28982b20db3d541313180ef22337574d57
|
||||
b9043b6c39124ffc60859515f594bd3c23d5f726 58601710028dd8d17eb837df915f1b38826b1fca
|
||||
b57e340389c1ab3ec99cc1bd04e3f0fba0b51351 9fc7202552ced5424f01fb2815e47d8ae98131d2 b9043b6c39124ffc60859515f594bd3c23d5f726
|
||||
9bc8d005fb6693dc7349ac2f9fee6594c6c662d5 58601710028dd8d17eb837df915f1b38826b1fca b57e340389c1ab3ec99cc1bd04e3f0fba0b51351
|
||||
25090aeb2a86baecc692a916120e6e169b97bcc5 9bc8d005fb6693dc7349ac2f9fee6594c6c662d5
|
||||
56fdaa1224fbdb768758889fd8e18fb3bfada309 25090aeb2a86baecc692a916120e6e169b97bcc5
|
||||
73d2b1ba930a48b379132e081ee2baed319b26f5 56fdaa1224fbdb768758889fd8e18fb3bfada309
|
||||
ec5267f5a5e2e3a06da379aa49fa8232a1afe732 b57e340389c1ab3ec99cc1bd04e3f0fba0b51351 73d2b1ba930a48b379132e081ee2baed319b26f5
|
||||
ba8c4cd2aa58c2add42ed1e007506a48c765768a 56fdaa1224fbdb768758889fd8e18fb3bfada309 ec5267f5a5e2e3a06da379aa49fa8232a1afe732
|
||||
a3b92711a971d123fd2f2053ca558dd62d20a083 ec5267f5a5e2e3a06da379aa49fa8232a1afe732
|
||||
00d6463de1cb5217e11dbb52c9a78b871f3020cd ec5267f5a5e2e3a06da379aa49fa8232a1afe732 a3b92711a971d123fd2f2053ca558dd62d20a083
|
||||
a77b5323289ef0e72fdbc1660c32d302c963d665 ba8c4cd2aa58c2add42ed1e007506a48c765768a 00d6463de1cb5217e11dbb52c9a78b871f3020cd
|
||||
37eec298d7c990b34dfe630f905c0f2d6144b275 a77b5323289ef0e72fdbc1660c32d302c963d665
|
||||
056bef7d5eb75d51b812f50b02df22fab39a8df8 37eec298d7c990b34dfe630f905c0f2d6144b275
|
||||
11554437854646c5ee26d3056c2b9e2d3f89de33 056bef7d5eb75d51b812f50b02df22fab39a8df8
|
||||
f6ae6bbdbbd3ff69e431b690c4da1bc31a46396d 37eec298d7c990b34dfe630f905c0f2d6144b275 11554437854646c5ee26d3056c2b9e2d3f89de33
|
||||
788e390e01efdda0513b617eceae64f424c97f11 f6ae6bbdbbd3ff69e431b690c4da1bc31a46396d
|
||||
544f0175d9968151df8fcd537f6399ea3fcb05fa 788e390e01efdda0513b617eceae64f424c97f11
|
||||
642487742c8ba27f1a24c68098bfaa81cff0677a 544f0175d9968151df8fcd537f6399ea3fcb05fa
|
||||
39132723dbc0bf3da3e5b022ad06866deafc44c7 642487742c8ba27f1a24c68098bfaa81cff0677a
|
||||
cf8994ceaf370677336e1d8c6d34f113f9e780e6 39132723dbc0bf3da3e5b022ad06866deafc44c7
|
||||
8affa0f3757cf92c1b5604499d6912101cf47cff 00d6463de1cb5217e11dbb52c9a78b871f3020cd cf8994ceaf370677336e1d8c6d34f113f9e780e6
|
||||
9fc5318e8644b6ada6fe127c97cfd2590ff97789 39132723dbc0bf3da3e5b022ad06866deafc44c7 8affa0f3757cf92c1b5604499d6912101cf47cff
|
||||
a14cdce07f9e39bd428a22f6c40b3849e0aadfc2 9fc5318e8644b6ada6fe127c97cfd2590ff97789
|
||||
09b704bcd73f4158b63218053c82f0af7dc3f14c 9fc5318e8644b6ada6fe127c97cfd2590ff97789
|
||||
d9f1f88107ee7b447a3088a3a6d76181f5fbb000 a14cdce07f9e39bd428a22f6c40b3849e0aadfc2 09b704bcd73f4158b63218053c82f0af7dc3f14c
|
||||
f260495495bf90c93dd0d958b19d925b57b62e3e d9f1f88107ee7b447a3088a3a6d76181f5fbb000
|
||||
d4cf95363f00b56453bd9ae58a20e4c9a39dd715 f260495495bf90c93dd0d958b19d925b57b62e3e
|
||||
e03618570d2da623a0a4e8823e4a8fe9a4bbca38 d4cf95363f00b56453bd9ae58a20e4c9a39dd715
|
||||
61e15e415519f6cae4cc94b3c3558d310b087f7b e03618570d2da623a0a4e8823e4a8fe9a4bbca38
|
||||
29c7295d16e99969ef39f8d338b88e143820745b d9f1f88107ee7b447a3088a3a6d76181f5fbb000 61e15e415519f6cae4cc94b3c3558d310b087f7b
|
||||
c9c910ab7cb5ad637ee7d01abf9e1a832ca088ff 29c7295d16e99969ef39f8d338b88e143820745b
|
||||
aba3c46eb1a985014ed56ca2d93853e97b36b8b4 c9c910ab7cb5ad637ee7d01abf9e1a832ca088ff
|
||||
94feb2ccaa55aacc21f2538b269ef514f92b3f81 aba3c46eb1a985014ed56ca2d93853e97b36b8b4
|
||||
18b0f5b790ef622350b7020bae71ff73b3e23166 94feb2ccaa55aacc21f2538b269ef514f92b3f81
|
||||
97bf9c257cbd52fbb0c519c93a141d894daf94df 8affa0f3757cf92c1b5604499d6912101cf47cff 18b0f5b790ef622350b7020bae71ff73b3e23166
|
||||
93d5289d25dd4785ecaf54ca19f921c0de03c60b 94feb2ccaa55aacc21f2538b269ef514f92b3f81 97bf9c257cbd52fbb0c519c93a141d894daf94df
|
||||
18b5ce8c1813c232467bc74e10b259799e068ccb 93d5289d25dd4785ecaf54ca19f921c0de03c60b
|
||||
9ab28d606af35cdace55be4921105516360db310 18b5ce8c1813c232467bc74e10b259799e068ccb
|
||||
486388a7988427a0140a6f89d1d14c4253202f06 9ab28d606af35cdace55be4921105516360db310
|
||||
15859f7be9f6fef815e6ad38f76634618e1dc6e0 486388a7988427a0140a6f89d1d14c4253202f06
|
||||
444b65d3713bc274c6ee27551d3ce0ef1f3837c8 15859f7be9f6fef815e6ad38f76634618e1dc6e0
|
||||
49bc62f0aa2872347d77e6503c5d412393bde03f 444b65d3713bc274c6ee27551d3ce0ef1f3837c8
|
||||
cc15ada3043b71986a2d48df8027b0e41ba7df5f 49bc62f0aa2872347d77e6503c5d412393bde03f
|
||||
86cc237c78778c27950906111dc797a87319ec01 cc15ada3043b71986a2d48df8027b0e41ba7df5f
|
||||
799987ecb19aa16cc31cbff5d7d96a99ef0fd3ee 86cc237c78778c27950906111dc797a87319ec01
|
||||
79b68923200dbe52bdf2b87ceffe41aec0323552 97bf9c257cbd52fbb0c519c93a141d894daf94df 799987ecb19aa16cc31cbff5d7d96a99ef0fd3ee
|
||||
f33cbce63fc3ca197477d8e42162feee0f1745a5 86cc237c78778c27950906111dc797a87319ec01 79b68923200dbe52bdf2b87ceffe41aec0323552
|
||||
2d57839b3ebf21e5bb92b9089c0241f8a774cc49 86cc237c78778c27950906111dc797a87319ec01
|
||||
1f7dd0287a4873810daace1c814cc464da6c60e2 f33cbce63fc3ca197477d8e42162feee0f1745a5 2d57839b3ebf21e5bb92b9089c0241f8a774cc49
|
||||
b6a87b9410f6a185f672147c283ea298f2b22c20 1f7dd0287a4873810daace1c814cc464da6c60e2
|
||||
2ae51c3f64e000c2d04b58e0b5c41f93be61c5b9 79b68923200dbe52bdf2b87ceffe41aec0323552 b6a87b9410f6a185f672147c283ea298f2b22c20
|
||||
bc30b012cf25ed3aa725191be5916f3f1e3026eb 1f7dd0287a4873810daace1c814cc464da6c60e2 2ae51c3f64e000c2d04b58e0b5c41f93be61c5b9
|
||||
ed28a4cc0d36d2e25096f1ed6863e2624b81dfff bc30b012cf25ed3aa725191be5916f3f1e3026eb
|
||||
133b36c34c4fe9ec3eb0b6bdb8d0f1f1a547d065 ed28a4cc0d36d2e25096f1ed6863e2624b81dfff
|
||||
1aa58eeaafd961fe987c8bd12be1b9207c4f6309 133b36c34c4fe9ec3eb0b6bdb8d0f1f1a547d065
|
||||
a6693481fa0fa92f2c1d7c768a2bb4c78e1024f9 1aa58eeaafd961fe987c8bd12be1b9207c4f6309
|
||||
53e199b20ff53845562a520da4bfaecf0189fad2 a6693481fa0fa92f2c1d7c768a2bb4c78e1024f9
|
||||
a8945bd3032c60ef3522b7aeb89f69b13d292931 53e199b20ff53845562a520da4bfaecf0189fad2
|
||||
8b6b6640d5c9066e34cc5bdaf9a89695afe37be0 2ae51c3f64e000c2d04b58e0b5c41f93be61c5b9 a8945bd3032c60ef3522b7aeb89f69b13d292931
|
||||
40cb76810e4daabd4d265e29226f20b93fd0e791 53e199b20ff53845562a520da4bfaecf0189fad2 8b6b6640d5c9066e34cc5bdaf9a89695afe37be0
|
||||
3054dfe79e2912a86c17ee61273728bad3a28a4b 40cb76810e4daabd4d265e29226f20b93fd0e791
|
||||
5a9fd0686eb6a1a89524449ab8711257c37b77dc 3054dfe79e2912a86c17ee61273728bad3a28a4b
|
||||
77e6b88c5dddfa18327a299cf39a61d1f7c2b0bb 5a9fd0686eb6a1a89524449ab8711257c37b77dc
|
||||
9f5d329105d30d0a48c47413fcf23dc8328ad526 77e6b88c5dddfa18327a299cf39a61d1f7c2b0bb
|
||||
eff483c1c47cda01da415f58b101f0348c61c200 9f5d329105d30d0a48c47413fcf23dc8328ad526
|
||||
54ba59872e27d4abd8e2ef6dd07e0805a4fce505 eff483c1c47cda01da415f58b101f0348c61c200
|
||||
eb796924b16fd84a073eab388c5ee23842d029f2 5a9fd0686eb6a1a89524449ab8711257c37b77dc 54ba59872e27d4abd8e2ef6dd07e0805a4fce505
|
||||
b6940eccffdafcfc213d6a7c4ba33ac29f7352ba 8b6b6640d5c9066e34cc5bdaf9a89695afe37be0 eb796924b16fd84a073eab388c5ee23842d029f2
|
||||
23e47c567a018f4f613f617aa2dc0711670b74a8 eb796924b16fd84a073eab388c5ee23842d029f2 b6940eccffdafcfc213d6a7c4ba33ac29f7352ba
|
||||
12c54f4bb3020f26177332c110e438abb0d46dae 23e47c567a018f4f613f617aa2dc0711670b74a8
|
||||
5d530edfabd1bce8d692ca5573464e2807bcd9f9 12c54f4bb3020f26177332c110e438abb0d46dae
|
||||
f87242452610692e4346ef632d4286ee6ee75311 5d530edfabd1bce8d692ca5573464e2807bcd9f9
|
||||
f64f3771997e87bf5aed8f1b348e4af39d7e2b7e 12c54f4bb3020f26177332c110e438abb0d46dae
|
||||
6233cb1e07633d75a9164bd247b7585c408a0a79 f64f3771997e87bf5aed8f1b348e4af39d7e2b7e
|
||||
9501b460c5c55c64a8af5714660ace9f2af88a88 6233cb1e07633d75a9164bd247b7585c408a0a79
|
||||
3c81e152e66b5ca7f63c71d5c9fab85a0e3a1247 9501b460c5c55c64a8af5714660ace9f2af88a88
|
||||
6dbdbf16379c0311c197bbac5c468f146e908fed 3c81e152e66b5ca7f63c71d5c9fab85a0e3a1247
|
||||
43403bc6f724c52cda644d91cbf1a8b581d691e1 6dbdbf16379c0311c197bbac5c468f146e908fed
|
||||
695270e515ea1ce8b2b4dd9758dfefe7f0a02770 f87242452610692e4346ef632d4286ee6ee75311 43403bc6f724c52cda644d91cbf1a8b581d691e1
|
||||
ecd3a97853b0f7a32648a35313c5ee2571452c71 695270e515ea1ce8b2b4dd9758dfefe7f0a02770
|
||||
98026e0685984a709dd9d9c659cb718b562df5ea b6940eccffdafcfc213d6a7c4ba33ac29f7352ba ecd3a97853b0f7a32648a35313c5ee2571452c71
|
||||
93c3dec66edb51a049c55f94ee432c5228cd24f3 695270e515ea1ce8b2b4dd9758dfefe7f0a02770 98026e0685984a709dd9d9c659cb718b562df5ea
|
||||
bf4d5fbc6b49f68afd9d49b3077f98da5c3336ff 93c3dec66edb51a049c55f94ee432c5228cd24f3
|
||||
e6a5fceeddefb8a750b38003a90c8ee1d58bdaec bf4d5fbc6b49f68afd9d49b3077f98da5c3336ff
|
||||
e4a7212f89cf6ffba37aa61162758fde6c8bdf15 e6a5fceeddefb8a750b38003a90c8ee1d58bdaec
|
||||
d4e520772e265544dbf8975d5307e43bf6e2f849 e4a7212f89cf6ffba37aa61162758fde6c8bdf15
|
||||
fea733a43ec0cdc9796cd86cdf19faac8f4be846 d4e520772e265544dbf8975d5307e43bf6e2f849
|
||||
5cd0a6e2f39828149dea886ee10f0c937b0e83de fea733a43ec0cdc9796cd86cdf19faac8f4be846
|
||||
3ee91eb6c84599bedf195bef5c01c2ff785d7e26 5cd0a6e2f39828149dea886ee10f0c937b0e83de
|
||||
0e83a5a98535b43d9ead28cfb0be472bf0ef3767 98026e0685984a709dd9d9c659cb718b562df5ea 3ee91eb6c84599bedf195bef5c01c2ff785d7e26
|
||||
faf8bd4a08db065bb85b0b642cc8114e8bf94725 5cd0a6e2f39828149dea886ee10f0c937b0e83de 0e83a5a98535b43d9ead28cfb0be472bf0ef3767
|
||||
088b772de59cc9638dd44ffe468d9e7650db62ff faf8bd4a08db065bb85b0b642cc8114e8bf94725
|
||||
8e100ff21bc0a90ac8033b3d2aecea79734ab605 088b772de59cc9638dd44ffe468d9e7650db62ff
|
||||
c1d4a73440a77f2993f4607ae4e849c3a31767d0 0e83a5a98535b43d9ead28cfb0be472bf0ef3767 8e100ff21bc0a90ac8033b3d2aecea79734ab605
|
||||
8202c94a438d9909d97219136f4d92b4e729938a 088b772de59cc9638dd44ffe468d9e7650db62ff c1d4a73440a77f2993f4607ae4e849c3a31767d0
|
||||
812c9b99d10013dd349146577519c5019185fdd3 8202c94a438d9909d97219136f4d92b4e729938a
|
||||
6b2e5b2e416abe260eeeea208454770d67ac5f9f 812c9b99d10013dd349146577519c5019185fdd3
|
||||
33dcd489ebe126c89f6fa92b6f55185111e17551 6b2e5b2e416abe260eeeea208454770d67ac5f9f
|
||||
efe1ac732eee4bb280a51bfcc3f809e27adcbf6c 33dcd489ebe126c89f6fa92b6f55185111e17551
|
||||
66aead387ebcbbbfa59d590dae96a5d255d43c25 c1d4a73440a77f2993f4607ae4e849c3a31767d0 efe1ac732eee4bb280a51bfcc3f809e27adcbf6c
|
||||
edab9e1b6b7089adf5447b76e2b082076d87c6b8 33dcd489ebe126c89f6fa92b6f55185111e17551 66aead387ebcbbbfa59d590dae96a5d255d43c25
|
||||
0da89d91dde7a7f77a4cf69e1dbd2ec3af619835 edab9e1b6b7089adf5447b76e2b082076d87c6b8
|
||||
d70f2fd196875282278d5455819dcfa238948f59 0da89d91dde7a7f77a4cf69e1dbd2ec3af619835
|
||||
b228c9477ee56f89a09cce3adf2340b28817150c 66aead387ebcbbbfa59d590dae96a5d255d43c25 d70f2fd196875282278d5455819dcfa238948f59
|
||||
a426f64795667bd144151c0ebec4ec57cfcb85f1 d70f2fd196875282278d5455819dcfa238948f59
|
||||
6aeebb9824b93261ba8270db34ebf8eac76b28b0 a426f64795667bd144151c0ebec4ec57cfcb85f1
|
||||
4e2e59ec87d6cad9ee9084b683323e094fbfa1ad 6aeebb9824b93261ba8270db34ebf8eac76b28b0
|
||||
f6a8de32150407469750a153ba07b3aaf0699a3c 4e2e59ec87d6cad9ee9084b683323e094fbfa1ad
|
||||
d29a7d6218772ee19d2fe1396fc5cf6e611f0ff4 f6a8de32150407469750a153ba07b3aaf0699a3c
|
||||
51e458ad571807c6edb5d4a1399eed6a031b537b d29a7d6218772ee19d2fe1396fc5cf6e611f0ff4
|
||||
867dbf41d5cc8ca3fde4dd539afb488a401f357d 51e458ad571807c6edb5d4a1399eed6a031b537b
|
||||
86abc7ea68faad8bca54fafcaeaf1f84a6478caf 867dbf41d5cc8ca3fde4dd539afb488a401f357d
|
||||
9219b2d411a40573e04bf69db37891720786e494 b228c9477ee56f89a09cce3adf2340b28817150c 86abc7ea68faad8bca54fafcaeaf1f84a6478caf
|
||||
5c362c14301b9273ed1dae7edcf373d50b7f9f24 867dbf41d5cc8ca3fde4dd539afb488a401f357d 9219b2d411a40573e04bf69db37891720786e494
|
||||
997e041042c1f9bdef18c0cfd1ffbf7a4a28695b 5c362c14301b9273ed1dae7edcf373d50b7f9f24
|
||||
5d6aa7c48a8116840ce35691cf08df885919a9ab 997e041042c1f9bdef18c0cfd1ffbf7a4a28695b
|
||||
2bc2660ad5cfe4d917e909663b065faef48b3fc5 5d6aa7c48a8116840ce35691cf08df885919a9ab
|
||||
a372e8150eafd25b90bd24df12c4942af56d6ccd 2bc2660ad5cfe4d917e909663b065faef48b3fc5
|
||||
8bdd0cc635765134d88d08f80c8d70c6b36235e2 a372e8150eafd25b90bd24df12c4942af56d6ccd
|
||||
8f05b979478b076b77e9e16023b7040ad07c5862 9219b2d411a40573e04bf69db37891720786e494 8bdd0cc635765134d88d08f80c8d70c6b36235e2
|
||||
583df9ee1f5d27a43c6fa121c81f974a4e9184da a372e8150eafd25b90bd24df12c4942af56d6ccd 8f05b979478b076b77e9e16023b7040ad07c5862
|
||||
388bea740bfde42852f8a4aee6c495c6d81c7251 583df9ee1f5d27a43c6fa121c81f974a4e9184da
|
||||
fd1346c5f402bae694090b409b4e38968ada7793 388bea740bfde42852f8a4aee6c495c6d81c7251
|
||||
d918fdb1373d326c4ce494168f5fa2ccc36a7dba fd1346c5f402bae694090b409b4e38968ada7793
|
||||
fced6719b10e7f26f96ed4e6d550347d032d7f04 8f05b979478b076b77e9e16023b7040ad07c5862 d918fdb1373d326c4ce494168f5fa2ccc36a7dba
|
||||
2d132913c37a42dd79086e760c14431837bc520b fd1346c5f402bae694090b409b4e38968ada7793 fced6719b10e7f26f96ed4e6d550347d032d7f04
|
||||
d98e67ac0cc9ceb4c51dcc96f80247c0bdafb1a2 2d132913c37a42dd79086e760c14431837bc520b
|
||||
2274d02be6b11cfa50d78e5689aa092f63d605f1 d98e67ac0cc9ceb4c51dcc96f80247c0bdafb1a2
|
||||
3cd97481ece8b0f0cf160819fe28187bace1c61c 2274d02be6b11cfa50d78e5689aa092f63d605f1
|
||||
89e6fb4c42d70fa5a44075a2023cc6b520c17d8c fced6719b10e7f26f96ed4e6d550347d032d7f04 3cd97481ece8b0f0cf160819fe28187bace1c61c
|
||||
7c73d1883ba2ee2a9fe39d7c1d90e19d9606b0a1 2274d02be6b11cfa50d78e5689aa092f63d605f1 89e6fb4c42d70fa5a44075a2023cc6b520c17d8c
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- run: go test ./storage -v
|
||||
- run: go test ./storage ./server -v
|
||||
- run: go test ./storage -bench=.
|
||||
|
||||
# build the assets
|
||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -2,6 +2,19 @@
|
||||
|
||||
Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Feature
|
||||
- Add REST API
|
||||
|
||||
### Testing
|
||||
- Add API tests
|
||||
|
||||
### UI
|
||||
- Changes to use new data API
|
||||
- Hide delete all / mark all read in message view
|
||||
|
||||
|
||||
## 1.1.7
|
||||
|
||||
### Fix
|
||||
|
@ -30,6 +30,7 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
|
||||
- Optional SMTP with STARTTLS & SMTP authentication ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication))
|
||||
- Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS))
|
||||
- Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication))
|
||||
- A simple REST API allowing ([see docs](docs/apiv1/README.md))
|
||||
- Multi-architecture [Docker images](https://github.com/axllent/mailpit/wiki/Docker-images)
|
||||
|
||||
|
||||
|
@ -55,6 +55,9 @@ var (
|
||||
|
||||
// SMTPAuth used for euthentication
|
||||
SMTPAuth *htpasswd.File
|
||||
|
||||
// ContentSecurityPolicy for HTTP server
|
||||
ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';"
|
||||
)
|
||||
|
||||
// VerifyConfig wil do some basic checking
|
||||
|
@ -20,7 +20,6 @@ type Message struct {
|
||||
Date time.Time
|
||||
Text string
|
||||
HTML string
|
||||
HTMLSource string
|
||||
Size int
|
||||
Inline []Attachment
|
||||
Attachments []Attachment
|
||||
|
80
docs/apiv1/Message.md
Normal file
80
docs/apiv1/Message.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Message
|
||||
|
||||
Returns a summary of the message and attachments.
|
||||
|
||||
**URL** : `api/v1/message/<ID>`
|
||||
|
||||
**Method** : `GET`
|
||||
|
||||
## Response
|
||||
|
||||
**Status** : `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"ID": "d7a5543b-96dd-478b-9b60-2b465c9884de",
|
||||
"Read": true,
|
||||
"From": {
|
||||
"Name": "John Doe",
|
||||
"Address": "john@example.com"
|
||||
},
|
||||
"To": [
|
||||
{
|
||||
"Name": "Jane Smith",
|
||||
"Address": "jane@example.com"
|
||||
}
|
||||
],
|
||||
"Cc": null,
|
||||
"Bcc": null,
|
||||
"Subject": "Message subject",
|
||||
"Date": "2016-09-07T16:46:00+13:00",
|
||||
"Text": "Plain text MIME part of the email",
|
||||
"HTML": "HTML MIME part (if exists)",
|
||||
"Size": 79499,
|
||||
"Inline": [
|
||||
{
|
||||
"PartID": "1.2",
|
||||
"FileName": "filename.gif",
|
||||
"ContentType": "image/gif",
|
||||
"ContentID": "919564503@07092006-1525",
|
||||
"Size": 7760
|
||||
}
|
||||
],
|
||||
"Attachments": [
|
||||
{
|
||||
"PartID": "2",
|
||||
"FileName": "filename.doc",
|
||||
"ContentType": "application/msword",
|
||||
"ContentID": "",
|
||||
"Size": 43520
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
### Notes
|
||||
|
||||
- `Read` - always true (message marked read on open)
|
||||
- `From` - Name & Address, or null
|
||||
- `To`, `CC`, `BCC` - Array of Names & Address, or null
|
||||
- `Date` - Parsed email local date & time from headers
|
||||
- `Size` - Total size of raw email
|
||||
- `Inline`, `Attachments` - Array of attachments and inline images.
|
||||
|
||||
|
||||
---
|
||||
## Attachments
|
||||
|
||||
**URL** : `api/v1/message/<ID>/part/<PartID>`
|
||||
|
||||
**Method** : `GET`
|
||||
|
||||
Returns the attachment using the MIME type provided by the attachment `ContentType`.
|
||||
|
||||
---
|
||||
## Raw (source) email
|
||||
|
||||
**URL** : `api/v1/message/<ID>/raw`
|
||||
|
||||
**Method** : `GET`
|
||||
|
||||
Returns the original email source including headers and attachments.
|
166
docs/apiv1/Messages.md
Normal file
166
docs/apiv1/Messages.md
Normal file
@ -0,0 +1,166 @@
|
||||
# Messages
|
||||
|
||||
List & delete messages.
|
||||
|
||||
|
||||
---
|
||||
## List
|
||||
|
||||
List messages in the mailbox. Messages are returned in the order of latest received to oldest.
|
||||
|
||||
**URL** : `api/v1/messages`
|
||||
|
||||
**Method** : `GET`
|
||||
|
||||
|
||||
### Query parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|---------|----------|----------------------------|
|
||||
| limit | integer | false | Limit results (default 50) |
|
||||
| start | integer | false | Pagination offset |
|
||||
|
||||
|
||||
### Response
|
||||
|
||||
**Status** : `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 500,
|
||||
"unread": 500,
|
||||
"count": 50,
|
||||
"start": 0,
|
||||
"messages": [
|
||||
{
|
||||
"ID": "1c575821-70ba-466f-8cee-2e1cf0fcdd0f",
|
||||
"Read": false,
|
||||
"From": {
|
||||
"Name": "John Doe",
|
||||
"Address": "john@example.com"
|
||||
},
|
||||
"To": [
|
||||
{
|
||||
"Name": "Jane Smith",
|
||||
"Address": "jane@example.com"
|
||||
}
|
||||
],
|
||||
"Cc": [
|
||||
{
|
||||
"Name": "Accounts",
|
||||
"Address": "accounts@example.com"
|
||||
}
|
||||
],
|
||||
"Bcc": null,
|
||||
"Subject": "Message subject",
|
||||
"Created": "2022-10-03T21:35:32.228605299+13:00",
|
||||
"Size": 6144,
|
||||
"Attachments": 0
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- `total` - Total messages in mailbox
|
||||
- `unread` - Total unread messages in mailbox
|
||||
- `count` - Number of messages returned in request
|
||||
- `start` - The offset (default `0`) for pagination
|
||||
- `Read` - The read/unread status of the message
|
||||
- `From` - Name & Address, or null if none
|
||||
- `To`, `CC`, `BCC` - Array of Names & Address, or null if none
|
||||
- `Created` - Local date & time the message was received
|
||||
- `Size` - Total size of raw email in bytes
|
||||
|
||||
|
||||
---
|
||||
## Delete individual messages
|
||||
|
||||
Delete one or more messages by ID.
|
||||
|
||||
**URL** : `api/v1/messages`
|
||||
|
||||
**Method** : `DELETE`
|
||||
|
||||
### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": ["<ID>","<ID>"...]
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
**Status** : `200`
|
||||
|
||||
|
||||
---
|
||||
## Delete all messages
|
||||
|
||||
Delete all messages (same as deleting individual messages, but with the "ids" either empty or omitted entirely).
|
||||
|
||||
**URL** : `api/v1/messages`
|
||||
|
||||
**Method** : `DELETE`
|
||||
|
||||
### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": []
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
**Status** : `200`
|
||||
|
||||
|
||||
---
|
||||
## Update individual read statuses
|
||||
|
||||
Set the read status of one or more messages.
|
||||
The `read` status can be `true` or `false`.
|
||||
|
||||
**URL** : `api/v1/messages`
|
||||
|
||||
**Method** : `PUT`
|
||||
|
||||
### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": ["<ID>","<ID>"...],
|
||||
"read": false
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
**Status** : `200`
|
||||
|
||||
---
|
||||
## Update all messages read status
|
||||
|
||||
Set the read status of all messages.
|
||||
The `read` status can be `true` or `false`.
|
||||
|
||||
**URL** : `api/v1/messages`
|
||||
|
||||
**Method** : `PUT`
|
||||
|
||||
### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": [],
|
||||
"read": false
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
**Status** : `200`
|
11
docs/apiv1/README.md
Normal file
11
docs/apiv1/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# API v1
|
||||
|
||||
Mailpit provides a simple REST API to access and delete stored messages.
|
||||
|
||||
If the Mailpit server is set to use Basic Authentication, then API requests must use Basic Authentication too.
|
||||
|
||||
The API is split into three main parts:
|
||||
|
||||
- [Messages](Messages.md) - Listing, deleting & marking messages as read/unread.
|
||||
- [Message](Message.md) - Return message data & attachments
|
||||
- [Search](Search.md) - Searching messages
|
67
docs/apiv1/Search.md
Normal file
67
docs/apiv1/Search.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Search
|
||||
|
||||
**URL** : `api/v1/search?query=<string>`
|
||||
|
||||
**Method** : `GET`
|
||||
|
||||
The search returns up to 200 of the most recent matches, and does not support pagination or limits.
|
||||
Matching messages are returned in the order of latest received to oldest.
|
||||
|
||||
|
||||
## Query parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|--------|----------|--------------|
|
||||
| query | string | true | Search query |
|
||||
|
||||
|
||||
## Response
|
||||
|
||||
**Status** : `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 500,
|
||||
"unread": 500,
|
||||
"count": 25,
|
||||
"start": 0,
|
||||
"messages": [
|
||||
{
|
||||
"ID": "1c575821-70ba-466f-8cee-2e1cf0fcdd0f",
|
||||
"Read": false,
|
||||
"From": {
|
||||
"Name": "John Doe",
|
||||
"Address": "john@example.com"
|
||||
},
|
||||
"To": [
|
||||
{
|
||||
"Name": "Jane Smith",
|
||||
"Address": "jane@example.com"
|
||||
}
|
||||
],
|
||||
"Cc": [
|
||||
{
|
||||
"Name": "Accounts",
|
||||
"Address": "accounts@example.com"
|
||||
}
|
||||
],
|
||||
"Bcc": null,
|
||||
"Subject": "Test email",
|
||||
"Created": "2022-10-03T21:35:32.228605299+13:00",
|
||||
"Size": 6144,
|
||||
"Attachments": 0
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- `total` - Total messages in mailbox (all messages, not search)
|
||||
- `unread` - Total unread messages in mailbox (all messages, not search)
|
||||
- `count` - Number of messages returned in request (up to 200 for search)
|
||||
- `start` - Always 0 (offset in search is unsupported)
|
||||
- `From` - Singular Name & Address, or null if none
|
||||
- `To`, `CC`, `BCC` - Array of Name & Address, or null if none
|
||||
- `Size` - Total size of raw email in bytes
|
279
server/api.go
279
server/api.go
@ -1,279 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/data"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type messagesResult struct {
|
||||
Total int `json:"total"`
|
||||
Unread int `json:"unread"`
|
||||
Count int `json:"count"`
|
||||
Start int `json:"start"`
|
||||
Items []data.Summary `json:"items"`
|
||||
}
|
||||
|
||||
// Return a list of available mailboxes
|
||||
func apiMailboxStats(w http.ResponseWriter, _ *http.Request) {
|
||||
res := storage.StatsGet()
|
||||
|
||||
bytes, _ := json.Marshal(res)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
|
||||
// List messages
|
||||
func apiListMessages(w http.ResponseWriter, r *http.Request) {
|
||||
start, limit := getStartLimit(r)
|
||||
|
||||
messages, err := storage.List(start, limit)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
stats := storage.StatsGet()
|
||||
|
||||
var res messagesResult
|
||||
|
||||
res.Start = start
|
||||
res.Items = messages
|
||||
res.Count = len(res.Items)
|
||||
res.Total = stats.Total
|
||||
res.Unread = stats.Unread
|
||||
|
||||
bytes, _ := json.Marshal(res)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
|
||||
// Search all messages
|
||||
func apiSearchMessages(w http.ResponseWriter, r *http.Request) {
|
||||
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||
if search == "" {
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := storage.Search(search)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
stats := storage.StatsGet()
|
||||
|
||||
var res messagesResult
|
||||
|
||||
res.Start = 0
|
||||
res.Items = messages
|
||||
res.Count = len(messages)
|
||||
res.Total = stats.Total
|
||||
res.Unread = stats.Unread
|
||||
|
||||
bytes, _ := json.Marshal(res)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
|
||||
// Open a message
|
||||
func apiOpenMessage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
msg, err := storage.GetMessage(id)
|
||||
if err != nil {
|
||||
httpError(w, "Message not found")
|
||||
return
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(msg)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
|
||||
// Download/view an attachment
|
||||
func apiDownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
partID := vars["partID"]
|
||||
|
||||
a, err := storage.GetAttachmentPart(id, partID)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
fileName := a.FileName
|
||||
if fileName == "" {
|
||||
fileName = a.ContentID
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", a.ContentType)
|
||||
w.Header().Set("Content-Disposition", "filename=\""+fileName+"\"")
|
||||
_, _ = w.Write(a.Content)
|
||||
}
|
||||
|
||||
// Download the full email source as plain text
|
||||
func apiDownloadRaw(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
dl := r.FormValue("dl")
|
||||
|
||||
data, err := storage.GetMessageRaw(id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if dl == "1" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
|
||||
}
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
// Delete all messages
|
||||
func apiDeleteAll(w http.ResponseWriter, r *http.Request) {
|
||||
err := storage.DeleteAllMessages()
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Delete all selected messages
|
||||
func apiDeleteSelected(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
||||
var data struct {
|
||||
IDs []string
|
||||
}
|
||||
err := decoder.Decode(&data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ids := data.IDs
|
||||
|
||||
for _, id := range ids {
|
||||
if err := storage.DeleteOneMessage(id); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Delete a single message
|
||||
func apiDeleteOne(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
err := storage.DeleteOneMessage(id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Mark single message as unread
|
||||
func apiUnreadOne(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
err := storage.MarkUnread(id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Mark all messages as read
|
||||
func apiMarkAllRead(w http.ResponseWriter, r *http.Request) {
|
||||
err := storage.MarkAllRead()
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Mark selected message as read
|
||||
func apiMarkSelectedRead(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
||||
var data struct {
|
||||
IDs []string
|
||||
}
|
||||
err := decoder.Decode(&data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ids := data.IDs
|
||||
|
||||
for _, id := range ids {
|
||||
if err := storage.MarkRead(id); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Mark selected message as unread
|
||||
func apiMarkSelectedUnread(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
||||
var data struct {
|
||||
IDs []string
|
||||
}
|
||||
err := decoder.Decode(&data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ids := data.IDs
|
||||
|
||||
for _, id := range ids {
|
||||
if err := storage.MarkUnread(id); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Websocket to broadcast changes
|
||||
func apiWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
websockets.ServeWs(websockets.MessageHub, w, r)
|
||||
}
|
289
server/apiv1/api.go
Normal file
289
server/apiv1/api.go
Normal file
@ -0,0 +1,289 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/data"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// MessagesResult struct
|
||||
type MessagesResult struct {
|
||||
Total int `json:"total"`
|
||||
Unread int `json:"unread"`
|
||||
Count int `json:"count"`
|
||||
Start int `json:"start"`
|
||||
Messages []data.Summary `json:"messages"`
|
||||
}
|
||||
|
||||
// // Mailbox returns an message overview (stats)
|
||||
// func Mailbox(w http.ResponseWriter, _ *http.Request) {
|
||||
// res := storage.StatsGet()
|
||||
|
||||
// bytes, _ := json.Marshal(res)
|
||||
// w.Header().Add("Content-Type", "application/json")
|
||||
// _, _ = w.Write(bytes)
|
||||
// }
|
||||
|
||||
// Messages returns a paginated list of messages
|
||||
func Messages(w http.ResponseWriter, r *http.Request) {
|
||||
start, limit := getStartLimit(r)
|
||||
|
||||
messages, err := storage.List(start, limit)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
stats := storage.StatsGet()
|
||||
|
||||
var res MessagesResult
|
||||
|
||||
res.Start = start
|
||||
res.Messages = messages
|
||||
res.Count = len(messages)
|
||||
res.Total = stats.Total
|
||||
res.Unread = stats.Unread
|
||||
|
||||
bytes, _ := json.Marshal(res)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
|
||||
// Search returns a max of 200 of the latest messages
|
||||
func Search(w http.ResponseWriter, r *http.Request) {
|
||||
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||
if search == "" {
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := storage.Search(search)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
stats := storage.StatsGet()
|
||||
|
||||
var res MessagesResult
|
||||
|
||||
res.Start = 0
|
||||
res.Messages = messages
|
||||
res.Count = len(messages)
|
||||
res.Total = stats.Total
|
||||
res.Unread = stats.Unread
|
||||
|
||||
bytes, _ := json.Marshal(res)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
|
||||
// Message (method: GET) returns a *data.Message
|
||||
func Message(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
msg, err := storage.GetMessage(id)
|
||||
if err != nil {
|
||||
httpError(w, "Message not found")
|
||||
return
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(msg)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
|
||||
// DownloadAttachment (method: GET) returns the attachment data
|
||||
func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
partID := vars["partID"]
|
||||
|
||||
a, err := storage.GetAttachmentPart(id, partID)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
fileName := a.FileName
|
||||
if fileName == "" {
|
||||
fileName = a.ContentID
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", a.ContentType)
|
||||
w.Header().Set("Content-Disposition", "filename=\""+fileName+"\"")
|
||||
_, _ = w.Write(a.Content)
|
||||
}
|
||||
|
||||
// DownloadRaw (method: GET) returns the full email source as plain text
|
||||
func DownloadRaw(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
dl := r.FormValue("dl")
|
||||
|
||||
data, err := storage.GetMessageRaw(id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if dl == "1" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
|
||||
}
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
// DeleteMessages (method: DELETE) deletes all messages matching IDS.
|
||||
// If no IDs are provided then all messages are deleted.
|
||||
func DeleteMessages(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var data struct {
|
||||
IDs []string
|
||||
}
|
||||
err := decoder.Decode(&data)
|
||||
if err != nil || len(data.IDs) == 0 {
|
||||
if err := storage.DeleteAllMessages(); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for _, id := range data.IDs {
|
||||
if err := storage.DeleteOneMessage(id); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// // DeleteMessage (method: DELETE) deletes a single message
|
||||
// func DeleteMessage(w http.ResponseWriter, r *http.Request) {
|
||||
// vars := mux.Vars(r)
|
||||
|
||||
// id := vars["id"]
|
||||
|
||||
// err := storage.DeleteOneMessage(id)
|
||||
// if err != nil {
|
||||
// httpError(w, err.Error())
|
||||
// return
|
||||
// }
|
||||
|
||||
// w.Header().Add("Content-Type", "text/plain")
|
||||
// _, _ = w.Write([]byte("ok"))
|
||||
// }
|
||||
|
||||
// SetAllRead (GET) will update all messages as read
|
||||
// func SetAllRead(w http.ResponseWriter, r *http.Request) {
|
||||
// err := storage.MarkAllRead()
|
||||
// if err != nil {
|
||||
// httpError(w, err.Error())
|
||||
// return
|
||||
// }
|
||||
|
||||
// w.Header().Add("Content-Type", "text/plain")
|
||||
// _, _ = w.Write([]byte("ok"))
|
||||
// }
|
||||
|
||||
// SetReadStatus (method: PUT) will update the status to Read/Unread for all provided IDs
|
||||
func SetReadStatus(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
||||
var data struct {
|
||||
Read bool
|
||||
IDs []string
|
||||
}
|
||||
|
||||
err := decoder.Decode(&data)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ids := data.IDs
|
||||
|
||||
if len(ids) == 0 {
|
||||
if data.Read {
|
||||
err := storage.MarkAllRead()
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := storage.MarkAllUnread()
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if data.Read {
|
||||
for _, id := range ids {
|
||||
if err := storage.MarkRead(id); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, id := range ids {
|
||||
if err := storage.MarkUnread(id); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// FourOFour returns a basic 404 message
|
||||
func fourOFour(w http.ResponseWriter) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprint(w, "404 page not found")
|
||||
}
|
||||
|
||||
// HTTPError returns a basic error message (400 response)
|
||||
func httpError(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprint(w, msg)
|
||||
}
|
||||
|
||||
// Get the start and limit based on query params. Defaults to 0, 50
|
||||
func getStartLimit(req *http.Request) (start int, limit int) {
|
||||
start = 0
|
||||
limit = 50
|
||||
|
||||
s := req.URL.Query().Get("start")
|
||||
if n, err := strconv.Atoi(s); err == nil && n > 0 {
|
||||
start = n
|
||||
}
|
||||
|
||||
l := req.URL.Query().Get("limit")
|
||||
if n, err := strconv.Atoi(l); err == nil && n > 0 {
|
||||
limit = n
|
||||
}
|
||||
|
||||
return start, limit
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package server
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -22,8 +22,8 @@ var (
|
||||
thumbHeight = 120
|
||||
)
|
||||
|
||||
// Attachment thumbnail (images only)
|
||||
func apiAttachmentThumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
// Thumbnail returns a thumbnail image for an attachment (images only)
|
||||
func Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
@ -3,17 +3,16 @@ package server
|
||||
import (
|
||||
"compress/gzip"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/mailpit/server/apiv1"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@ -21,8 +20,6 @@ import (
|
||||
//go:embed ui
|
||||
var embeddedFS embed.FS
|
||||
|
||||
var contentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';"
|
||||
|
||||
// Listen will start the httpd
|
||||
func Listen() {
|
||||
serverRoot, err := fs.Sub(embeddedFS, "ui")
|
||||
@ -35,22 +32,12 @@ func Listen() {
|
||||
|
||||
go websockets.MessageHub.Run()
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/api/stats", middleWareFunc(apiMailboxStats)).Methods("GET")
|
||||
r.HandleFunc("/api/messages", middleWareFunc(apiListMessages)).Methods("GET")
|
||||
r.HandleFunc("/api/search", middleWareFunc(apiSearchMessages)).Methods("GET")
|
||||
r.HandleFunc("/api/delete", middleWareFunc(apiDeleteAll)).Methods("GET")
|
||||
r.HandleFunc("/api/delete", middleWareFunc(apiDeleteSelected)).Methods("POST")
|
||||
r := defaultRoutes()
|
||||
|
||||
// web UI websocket
|
||||
r.HandleFunc("/api/events", apiWebsocket).Methods("GET")
|
||||
r.HandleFunc("/api/read", apiMarkAllRead).Methods("GET")
|
||||
r.HandleFunc("/api/read", apiMarkSelectedRead).Methods("POST")
|
||||
r.HandleFunc("/api/unread", apiMarkSelectedUnread).Methods("POST")
|
||||
r.HandleFunc("/api/{id}/raw", middleWareFunc(apiDownloadRaw)).Methods("GET")
|
||||
r.HandleFunc("/api/{id}/part/{partID}", middleWareFunc(apiDownloadAttachment)).Methods("GET")
|
||||
r.HandleFunc("/api/{id}/part/{partID}/thumb", middleWareFunc(apiAttachmentThumbnail)).Methods("GET")
|
||||
r.HandleFunc("/api/{id}/delete", middleWareFunc(apiDeleteOne)).Methods("GET")
|
||||
r.HandleFunc("/api/{id}/unread", middleWareFunc(apiUnreadOne)).Methods("GET")
|
||||
r.HandleFunc("/api/{id}", middleWareFunc(apiOpenMessage)).Methods("GET")
|
||||
|
||||
// virtual filesystem for others
|
||||
r.PathPrefix("/").Handler(middlewareHandler(http.FileServer(http.FS(serverRoot))))
|
||||
http.Handle("/", r)
|
||||
|
||||
@ -67,6 +54,22 @@ func Listen() {
|
||||
}
|
||||
}
|
||||
|
||||
func defaultRoutes() *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
|
||||
// API V1
|
||||
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.Messages)).Methods("GET")
|
||||
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT")
|
||||
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
|
||||
r.HandleFunc("/api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
|
||||
r.HandleFunc("/api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
|
||||
r.HandleFunc("/api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
|
||||
r.HandleFunc("/api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
|
||||
r.HandleFunc("/api/v1/message/{id}", middleWareFunc(apiv1.Message)).Methods("GET")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// BasicAuthResponse returns an basic auth response to the browser
|
||||
func basicAuthResponse(w http.ResponseWriter) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Login"`)
|
||||
@ -88,7 +91,7 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
@ -121,7 +124,7 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
func middlewareHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
@ -148,38 +151,7 @@ func middlewareHandler(h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// FourOFour returns a basic 404 message
|
||||
func fourOFour(w http.ResponseWriter) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprint(w, "404 page not found")
|
||||
}
|
||||
|
||||
// HTTPError returns a basic error message (400 response)
|
||||
func httpError(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprint(w, msg)
|
||||
}
|
||||
|
||||
// Get the start and limit based on query params. Defaults to 0, 50
|
||||
func getStartLimit(req *http.Request) (start int, limit int) {
|
||||
start = 0
|
||||
limit = 50
|
||||
|
||||
s := req.URL.Query().Get("start")
|
||||
if n, err := strconv.Atoi(s); err == nil && n > 0 {
|
||||
start = n
|
||||
}
|
||||
|
||||
l := req.URL.Query().Get("limit")
|
||||
if n, err := strconv.Atoi(l); err == nil && n > 0 {
|
||||
limit = n
|
||||
}
|
||||
|
||||
return start, limit
|
||||
// Websocket to broadcast changes
|
||||
func apiWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
websockets.ServeWs(websockets.MessageHub, w, r)
|
||||
}
|
||||
|
305
server/server_test.go
Normal file
305
server/server_test.go
Normal file
@ -0,0 +1,305 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/server/apiv1"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
var (
|
||||
putDataStruct struct {
|
||||
Read bool `json:"read"`
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
)
|
||||
|
||||
func Test_APIv1(t *testing.T) {
|
||||
setup()
|
||||
defer storage.Close()
|
||||
|
||||
r := defaultRoutes()
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
m, err := fetchMessages(ts.URL + "/api/v1/messages")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
// check count of empty database
|
||||
assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0)
|
||||
|
||||
// insert 100
|
||||
t.Log("Insert 100 messages")
|
||||
insertEmailData(t)
|
||||
assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)
|
||||
|
||||
// store this for later tests
|
||||
|
||||
m, err = fetchMessages(ts.URL + "/api/v1/messages")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
// read first 10
|
||||
t.Log("Read first 10 messages")
|
||||
putIDS := []string{}
|
||||
for indx, msg := range m.Messages {
|
||||
if indx == 10 {
|
||||
break
|
||||
}
|
||||
|
||||
_, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
// store for later
|
||||
putIDS = append(putIDS, msg.ID)
|
||||
}
|
||||
assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100)
|
||||
|
||||
// mark first 10 as unread
|
||||
t.Log("Mark first 10 as unread")
|
||||
putData := putDataStruct
|
||||
putData.IDs = putIDS
|
||||
j, err := json.Marshal(putData)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
_, err = clientPut(ts.URL+"/api/v1/messages", string(j))
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)
|
||||
|
||||
// mark first 10 as read
|
||||
t.Log("Mark first 10 as read")
|
||||
putData.Read = true
|
||||
j, err = json.Marshal(putData)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
_, err = clientPut(ts.URL+"/api/v1/messages", string(j))
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100)
|
||||
|
||||
// search
|
||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "from-1@example.com", 1)
|
||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "to:from-1@example.com", 0)
|
||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "from:@example.com", 100)
|
||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line\"", 100)
|
||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line 17 end\"", 1)
|
||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "!thisdoesnotexist", 100)
|
||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "-thisdoesnotexist", 100)
|
||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "thisdoesnotexist", 0)
|
||||
|
||||
// delete first 10
|
||||
t.Log("Delete first 10")
|
||||
_, err = clientDelete(ts.URL+"/api/v1/messages", string(j))
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 90)
|
||||
|
||||
// mark all as read
|
||||
putData.Read = true
|
||||
putData.IDs = []string{}
|
||||
j, err = json.Marshal(putData)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
t.Log("Mark all read")
|
||||
_, err = clientPut(ts.URL+"/api/v1/messages", string(j))
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 90)
|
||||
|
||||
// delete all
|
||||
t.Log("Delete all messages")
|
||||
_, err = clientDelete(ts.URL+"/api/v1/messages", "{}")
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, received %s", err.Error())
|
||||
}
|
||||
assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0)
|
||||
}
|
||||
|
||||
func setup() {
|
||||
config.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
|
||||
if err := storage.InitDB(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertStatsEqual(t *testing.T, uri string, unread, total int) {
|
||||
m := apiv1.MessagesResult{}
|
||||
|
||||
data, err := clientGet(uri)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
t.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assertEqual(t, unread, m.Unread, "wrong unread count")
|
||||
assertEqual(t, total, m.Total, "wrong total count")
|
||||
}
|
||||
|
||||
func assertSearchEqual(t *testing.T, uri, query string, count int) {
|
||||
t.Logf("Test search: %s", query)
|
||||
m := apiv1.MessagesResult{}
|
||||
|
||||
data, err := clientGet(uri + "?query=" + url.QueryEscape(query))
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
t.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assertEqual(t, count, m.Count, "wrong search results count")
|
||||
}
|
||||
|
||||
func insertEmailData(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
msg := enmime.Builder().
|
||||
From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)).
|
||||
Subject(fmt.Sprintf("Subject line %d end", i)).
|
||||
Text([]byte(fmt.Sprintf("This is the email body %d <jdsauk;dwqmdqw;>.", i))).
|
||||
To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i))
|
||||
|
||||
env, err := msg.Build()
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if err := env.Encode(buf); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if _, err := storage.Store(buf.Bytes()); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func fetchMessages(url string) (apiv1.MessagesResult, error) {
|
||||
m := apiv1.MessagesResult{}
|
||||
|
||||
data, err := clientGet(url)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func clientGet(url string) ([]byte, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
func clientDelete(url, body string) ([]byte, error) {
|
||||
client := new(http.Client)
|
||||
|
||||
b := strings.NewReader(body)
|
||||
req, err := http.NewRequest("DELETE", url, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
func clientPut(url, body string) ([]byte, error) {
|
||||
client := new(http.Client)
|
||||
|
||||
b := strings.NewReader(body)
|
||||
req, err := http.NewRequest("PUT", url, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
if a == b {
|
||||
return
|
||||
}
|
||||
message = fmt.Sprintf("%s: \"%v\" != \"%v\"", message, a, b)
|
||||
t.Fatal(message)
|
||||
}
|
@ -84,11 +84,11 @@ export default {
|
||||
let params = {};
|
||||
this.selected = [];
|
||||
|
||||
let uri = 'api/messages';
|
||||
let uri = 'api/v1/messages';
|
||||
if (self.search) {
|
||||
self.searching = true;
|
||||
self.items = [];
|
||||
uri = 'api/search'
|
||||
uri = 'api/v1/search'
|
||||
self.start = 0; // search is displayed on one page
|
||||
params['query'] = self.search;
|
||||
} else {
|
||||
@ -104,7 +104,12 @@ export default {
|
||||
self.unread = response.data.unread;
|
||||
self.count = response.data.count;
|
||||
self.start = response.data.start;
|
||||
self.items = response.data.items;
|
||||
self.items = response.data.messages;
|
||||
|
||||
if (self.items == 0 && self.start > 0) {
|
||||
self.start = 0;
|
||||
return self.loadMessages();
|
||||
}
|
||||
|
||||
if (!self.scrollInPlace) {
|
||||
let mp = document.getElementById('message-page');
|
||||
@ -153,7 +158,7 @@ export default {
|
||||
let self = this;
|
||||
self.selected = [];
|
||||
|
||||
let uri = 'api/' + self.currentPath
|
||||
let uri = 'api/v1/message/' + self.currentPath
|
||||
self.get(uri, false, function(response) {
|
||||
for (let i in self.items) {
|
||||
if (self.items[i].ID == self.currentPath) {
|
||||
@ -171,14 +176,14 @@ export default {
|
||||
if (a.ContentID != '') {
|
||||
d.HTML = d.HTML.replace(
|
||||
new RegExp('cid:'+a.ContentID, 'g'),
|
||||
window.location.origin+'/api/'+d.ID+'/part/'+a.PartID
|
||||
window.location.origin+'/api/v1/message/'+d.ID+'/part/'+a.PartID
|
||||
);
|
||||
}
|
||||
if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) {
|
||||
// some old email clients use the filename
|
||||
d.HTML = d.HTML.replace(
|
||||
new RegExp('src=(\'|")'+a.FileName+'(\'|")', 'g'),
|
||||
'src="'+window.location.origin+'/api/'+d.ID+'/part/'+a.PartID+'"'
|
||||
'src="'+window.location.origin+'/api/v1/message/'+d.ID+'/part/'+a.PartID+'"'
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -190,14 +195,14 @@ export default {
|
||||
if (a.ContentID != '') {
|
||||
d.HTML = d.HTML.replace(
|
||||
new RegExp('cid:'+a.ContentID, 'g'),
|
||||
window.location.origin+'/api/'+d.ID+'/part/'+a.PartID
|
||||
window.location.origin+'/api/v1/message/'+d.ID+'/part/'+a.PartID
|
||||
);
|
||||
}
|
||||
if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) {
|
||||
// some old email clients use the filename
|
||||
d.HTML = d.HTML.replace(
|
||||
new RegExp('src=(\'|")'+a.FileName+'(\'|")', 'g'),
|
||||
'src="'+window.location.origin+'/api/'+d.ID+'/part/'+a.PartID+'"'
|
||||
'src="'+window.location.origin+'/api/v1/message/'+d.ID+'/part/'+a.PartID+'"'
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -221,48 +226,42 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// universal handler to delete current or selected messages
|
||||
deleteMessages: function() {
|
||||
let ids = [];
|
||||
let self = this;
|
||||
if (self.message) {
|
||||
ids.push(self.message.ID);
|
||||
} else {
|
||||
ids = JSON.parse(JSON.stringify(self.selected));
|
||||
}
|
||||
if (!ids.length) {
|
||||
return false;
|
||||
}
|
||||
let uri = 'api/v1/messages';
|
||||
self.delete(uri, {'ids': ids}, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
});
|
||||
},
|
||||
|
||||
deleteAll: function() {
|
||||
let self = this;
|
||||
let uri = 'api/delete'
|
||||
self.get(uri, false, function(response) {
|
||||
let uri = 'api/v1/messages';
|
||||
self.delete(uri, false, function(response) {
|
||||
window.location.hash = "";
|
||||
self.reloadMessages();
|
||||
});
|
||||
},
|
||||
|
||||
deleteOne: function() {
|
||||
let self = this;
|
||||
if (!self.message) {
|
||||
return false;
|
||||
}
|
||||
let uri = 'api/' + self.message.ID + '/delete'
|
||||
self.get(uri, false, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
});
|
||||
},
|
||||
|
||||
deleteSelected: function() {
|
||||
let self = this;
|
||||
if (!self.selected.length) {
|
||||
return false;
|
||||
}
|
||||
let uri = 'api/delete'
|
||||
self.post(uri, {'ids': self.selected}, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
});
|
||||
},
|
||||
|
||||
markUnread: function() {
|
||||
let self = this;
|
||||
if (!self.message) {
|
||||
return false;
|
||||
}
|
||||
let uri = 'api/' + self.message.ID + '/unread'
|
||||
self.get(uri, false, function(response) {
|
||||
let uri = 'api/v1/messages';
|
||||
self.put(uri, {'read': false, 'ids': [self.message.ID]}, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
@ -271,8 +270,8 @@ export default {
|
||||
|
||||
markAllRead: function() {
|
||||
let self = this;
|
||||
let uri = 'api/read'
|
||||
self.get(uri, false, function(response) {
|
||||
let uri = 'api/v1/messages'
|
||||
self.put(uri, {'read': true}, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
@ -284,8 +283,8 @@ export default {
|
||||
if (!self.selected.length) {
|
||||
return false;
|
||||
}
|
||||
let uri = 'api/read'
|
||||
self.post(uri, {'ids': self.selected}, function(response) {
|
||||
let uri = 'api/v1/messages';
|
||||
self.put(uri, {'read': true, 'ids': self.selected}, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
@ -297,14 +296,40 @@ export default {
|
||||
if (!self.selected.length) {
|
||||
return false;
|
||||
}
|
||||
let uri = 'api/unread'
|
||||
self.post(uri, {'ids': self.selected}, function(response) {
|
||||
let uri = 'api/v1/messages';
|
||||
self.put(uri, {'read': false, 'ids': self.selected}, function(response) {
|
||||
window.location.hash = "";
|
||||
self.scrollInPlace = true;
|
||||
self.loadMessages();
|
||||
});
|
||||
},
|
||||
|
||||
// test of any selected emails are unread
|
||||
selectedHasUnread: function() {
|
||||
if (!this.selected.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i in this.items) {
|
||||
if (this.isSelected(this.items[i].ID) && !this.items[i].Read) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// test of any selected emails are read
|
||||
selectedHasRead: function() {
|
||||
if (!this.selected.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i in this.items) {
|
||||
if (this.isSelected(this.items[i].ID) && this.items[i].Read) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// websocket connect
|
||||
connect: function () {
|
||||
let wsproto = location.protocol == 'https:' ? 'wss' : 'ws';
|
||||
@ -331,7 +356,8 @@ export default {
|
||||
}
|
||||
self.total++;
|
||||
self.unread++;
|
||||
self.browserNotify("New mail from: " + response.Data.From.Address, response.Data.Subject);
|
||||
let from = response.Data.From != null ? response.Data.From.Address : '[unknown]';
|
||||
self.browserNotify("New mail from: " + from, response.Data.Subject);
|
||||
} else if (response.Type == "prune") {
|
||||
// messages have been deleted, reload messages to adjust
|
||||
self.scrollInPlace = true;
|
||||
@ -471,19 +497,19 @@ export default {
|
||||
<a class="btn btn-outline-secondary me-4 px-3" href="#" v-on:click="message=false" title="Return to messages">
|
||||
<i class="bi bi-arrow-return-left"></i>
|
||||
</a>
|
||||
<button class="btn btn-outline-secondary me-2" title="Delete message" v-on:click="deleteOne">
|
||||
<i class="bi bi-trash-fill"></i> <span class="d-none d-md-inline">Delete</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary me-2" title="Mark unread" v-on:click="markUnread">
|
||||
<i class="bi bi-eye-slash"></i> <span class="d-none d-md-inline">Mark unread</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary me-2" title="Delete message" v-on:click="deleteMessages">
|
||||
<i class="bi bi-trash-fill"></i> <span class="d-none d-md-inline">Delete</span>
|
||||
</button>
|
||||
<a class="btn btn-outline-secondary float-end" :class="messageNext ? '':'disabled'" :href="'#'+messageNext" title="View next message">
|
||||
<i class="bi bi-caret-right-fill"></i>
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary ms-2 me-1 float-end" :class="messagePrev ? '': 'disabled'" :href="'#'+messagePrev" title="View previous message">
|
||||
<i class="bi bi-caret-left-fill"></i>
|
||||
</a>
|
||||
<a :href="'api/' + message.ID + '/raw?dl=1'" class="btn btn-outline-secondary me-2 float-end" title="Download message">
|
||||
<a :href="'api/v1/' + message.ID + '/raw?dl=1'" class="btn btn-outline-secondary me-2 float-end" title="Download message">
|
||||
<i class="bi bi-file-arrow-down-fill"></i> <span class="d-none d-md-inline">Download</span>
|
||||
</a>
|
||||
</div>
|
||||
@ -556,13 +582,13 @@ export default {
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="my-3" v-if="unread && !selected.length">
|
||||
<li class="my-3" v-if="!message && unread && !selected.length">
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#MarkAllReadModal">
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
Mark all read
|
||||
</a>
|
||||
</li>
|
||||
<li class="my-3" v-if="total && !selected.length">
|
||||
<li class="my-3" v-if="!message && total && !selected.length">
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#DeleteAllModal">
|
||||
<i class="bi bi-trash-fill me-1 text-danger"></i>
|
||||
Delete all
|
||||
@ -573,20 +599,20 @@ export default {
|
||||
<b class="me-2">Selected {{selected.length}}</b>
|
||||
<button class="btn btn-sm text-muted" v-on:click="selected=[]" title="Unselect messages"><i class="bi bi-x-circle"></i></button>
|
||||
</li>
|
||||
<li class="my-3 ms-2" v-if="unread && selected.length > 0">
|
||||
<li class="my-3 ms-2" v-if="selected.length > 0 && selectedHasUnread()">
|
||||
<a href="#" v-on:click="markSelectedRead">
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
Mark read
|
||||
</a>
|
||||
</li>
|
||||
<li class="my-3 ms-2" v-if="selected.length > 0">
|
||||
<li class="my-3 ms-2" v-if="selected.length > 0 && selectedHasRead()">
|
||||
<a href="#" v-on:click="markSelectedUnread">
|
||||
<i class="bi bi-eye-slash"></i>
|
||||
Mark unread
|
||||
</a>
|
||||
</li>
|
||||
<li class="my-3 ms-2" v-if="total && selected.length > 0">
|
||||
<a href="#" v-on:click="deleteSelected">
|
||||
<a href="#" v-on:click="deleteMessages">
|
||||
<i class="bi bi-trash-fill me-1 text-danger"></i>
|
||||
Delete
|
||||
</a>
|
||||
|
@ -88,16 +88,16 @@ const commonMixins = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Axios Post request
|
||||
* Axios POST request
|
||||
*
|
||||
* @params string url
|
||||
* @params array array parameters Object/array
|
||||
* @params array object/array values
|
||||
* @params function callback function
|
||||
*/
|
||||
post: function (url, values, callback) {
|
||||
post: function (url, data, callback) {
|
||||
let self = this;
|
||||
self.loading++;
|
||||
axios.post(url, values)
|
||||
axios.post(url, data)
|
||||
.then(callback)
|
||||
.catch(self.handleError)
|
||||
.then(function () {
|
||||
@ -112,13 +112,34 @@ const commonMixins = {
|
||||
* Axios DELETE request (REST only)
|
||||
*
|
||||
* @params string url
|
||||
* @params array array parameters Object/array
|
||||
* @params array object/array values
|
||||
* @params function callback function
|
||||
*/
|
||||
delete: function (url, values, callback) {
|
||||
delete: function (url, data, callback) {
|
||||
let self = this;
|
||||
self.loading++;
|
||||
axios.delete(url, { data: values })
|
||||
axios.delete(url, { data: data })
|
||||
.then(callback)
|
||||
.catch(self.handleError)
|
||||
.then(function () {
|
||||
// always executed
|
||||
if (self.loading > 0) {
|
||||
self.loading--;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Axios PUT request (REST only)
|
||||
*
|
||||
* @params string url
|
||||
* @params array object/array values
|
||||
* @params function callback function
|
||||
*/
|
||||
put: function (url, data, callback) {
|
||||
let self = this;
|
||||
self.loading++;
|
||||
axios.put(url, data)
|
||||
.then(callback)
|
||||
.catch(self.handleError)
|
||||
.then(function () {
|
||||
|
@ -14,8 +14,8 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="mt-4 border-top pt-4">
|
||||
<a v-for="part in attachments" :href="'api/'+message.ID+'/part/'+part.PartID" class="card attachment float-start me-3 mb-3" target="_blank" style="width: 180px">
|
||||
<img v-if="isImage(part)" :src="'api/'+message.ID+'/part/'+part.PartID+'/thumb'" class="card-img-top" alt="">
|
||||
<a v-for="part in attachments" :href="'api/v1/message/'+message.ID+'/part/'+part.PartID" class="card attachment float-start me-3 mb-3" target="_blank" style="width: 180px">
|
||||
<img v-if="isImage(part)" :src="'api/v1/message/'+message.ID+'/part/'+part.PartID+'/thumb'" class="card-img-top" alt="">
|
||||
<img v-else src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAB4AQMAAABhKUq+AAAAA1BMVEX///+nxBvIAAAAGUlEQVQYGe3BgQAAAADDoPtTT+EA1QAAgFsLQAAB12s2WgAAAABJRU5ErkJggg==" class="card-img-top" alt="">
|
||||
<div class="icon" v-if="!isImage(part)">
|
||||
<i class="bi" :class="attachmentIcon(part)"></i>
|
||||
|
@ -44,7 +44,7 @@ export default {
|
||||
self.renderUI();
|
||||
var tabEl = document.getElementById('nav-raw-tab');
|
||||
tabEl.addEventListener('shown.bs.tab', function (event) {
|
||||
self.srcURI = 'api/' + self.message.ID + '/raw';
|
||||
self.srcURI = 'api/v1/message/' + self.message.ID + '/raw';
|
||||
});
|
||||
},
|
||||
|
||||
@ -174,7 +174,7 @@ export default {
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-for="part in allAttachments(message)">
|
||||
<a :href="'api/'+message.ID+'/part/'+part.PartID" type="button"
|
||||
<a :href="'api/v1/message/'+message.ID+'/part/'+part.PartID" type="button"
|
||||
class="dropdown-item" target="_blank">
|
||||
<i class="bi" :class="attachmentIcon(part)"></i>
|
||||
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
|
||||
@ -193,7 +193,7 @@ export default {
|
||||
aria-selected="true" v-if="message.HTML">HTML</button>
|
||||
<button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#nav-html-source" type="button" role="tab" aria-controls="nav-html-source"
|
||||
aria-selected="false" v-if="message.HTMLSource">HTML Source</button>
|
||||
aria-selected="false" v-if="message.HTML">HTML Source</button>
|
||||
<button class="nav-link" id="nav-plain-text-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#nav-plain-text" type="button" role="tab" aria-controls="nav-plain-text"
|
||||
aria-selected="false" :class="message.HTML == '' ? 'show':''">Text</button>
|
||||
@ -211,8 +211,8 @@ export default {
|
||||
<Attachments v-if="allAttachments(message).length" :message="message" :attachments="allAttachments(message)"></Attachments>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-html-source" role="tabpanel"
|
||||
aria-labelledby="nav-html-source-tab" tabindex="0" v-if="message.HTMLSource">
|
||||
<pre><code class="language-html">{{ message.HTMLSource }}</code></pre>
|
||||
aria-labelledby="nav-html-source-tab" tabindex="0" v-if="message.HTML">
|
||||
<pre><code class="language-html">{{ message.HTML }}</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel"
|
||||
aria-labelledby="nav-plain-text-tab" tabindex="0" :class="message.HTML == '' ? 'show':''">
|
||||
|
@ -1,97 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="500"
|
||||
height="460"
|
||||
viewBox="0 0 132.29167 121.70833"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="mailpit.svg"
|
||||
inkscape:export-filename="/home/ralph/bitmap.png"
|
||||
inkscape:export-xdpi="176.09"
|
||||
inkscape:export-ydpi="176.09">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="90.98717"
|
||||
inkscape:cy="229.51456"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer2"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
units="px"
|
||||
inkscape:window-width="1548"
|
||||
inkscape:window-height="838"
|
||||
inkscape:window-x="52"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1">
|
||||
<sodipodi:guide
|
||||
position="39.014182,62.44412"
|
||||
orientation="0,1"
|
||||
id="guide4529"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 2"
|
||||
style="display:inline"
|
||||
transform="translate(-55.479864,-26.541592)">
|
||||
<g
|
||||
id="g4547"
|
||||
transform="matrix(1.9570423,0,0,1.9490788,-53.096581,-140.70068)"
|
||||
style="opacity:1">
|
||||
<path
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4534"
|
||||
d="M 61.775483,85.805801 89.296873,113.46893 116.98363,85.8058 Z"
|
||||
style="fill:#2d4a5f;fill-opacity:0.94117647;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4540"
|
||||
d="m 58.113837,90.436008 31.088544,30.616072 31.277529,-30.521576 -0.0945,18.898806 -30.71057,12.56771 7.748511,6.47285 -4.157737,3.07105 -21.26116,0.0945 c -2.471939,-0.0114 -13.222442,-9.40933 -13.890627,-21.16666 z"
|
||||
style="fill:#2d4a5f;fill-opacity:0.94117647;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccczzcccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4542"
|
||||
d="m 95.532643,122.7713 27.544977,-11.12272 -4.10354,29.40775 -6.05271,-4.68532 c -11.10189,11.88809 -23.124233,13.48775 -34.745034,10.69078 -11.620801,-2.79697 -16.420919,-10.7759 -20.062499,-18.2612 -3.64158,-7.4853 -2.976265,-15.74301 -1.181174,-23.10379 0.577547,5.393 -0.671158,8.37123 3.260045,17.24516 3.224283,5.84857 7.36483,10.47545 13.229166,12.80395 7.102803,3.17859 16.477397,1.7222 21.308409,-1.55916 l 7.276037,-6.2366 z"
|
||||
style="fill:#00b786;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="460" viewBox="0 0 132.292 121.708" xmlns:v="https://vecta.io/nano"><path d="M12.321 0l53.861 53.918L120.365 0zM5.155 9.025l60.842 59.673 61.211-59.489-.185 36.835L66.921 70.54l15.164 12.616-8.137 5.986-41.609.184c-4.838-.022-25.877-18.34-27.185-41.255z" fill-opacity=".941" fill="#2d4a5f"/><path d="M78.385 72.049l53.907-21.679-8.031 57.318-11.845-9.132c-21.727 23.171-45.255 26.289-67.997 20.837S12.281 98.39 5.155 83.8-.67 53.116 2.843 38.769c1.13 10.511-1.313 16.316 6.38 33.612 6.31 11.399 14.413 20.417 25.89 24.956 13.9 6.195 32.247 3.357 41.701-3.039l14.24-12.156z" fill="#00b786"/></svg>
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 655 B |
@ -370,17 +370,16 @@ func GetMessage(id string) (*data.Message, error) {
|
||||
date, _ := env.Date()
|
||||
|
||||
obj := data.Message{
|
||||
ID: id,
|
||||
Read: true,
|
||||
From: from,
|
||||
Date: date,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Size: len(raw),
|
||||
Text: env.Text,
|
||||
HTMLSource: env.HTML,
|
||||
ID: id,
|
||||
Read: true,
|
||||
From: from,
|
||||
Date: date,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Size: len(raw),
|
||||
Text: env.Text,
|
||||
}
|
||||
|
||||
html := env.HTML
|
||||
@ -388,6 +387,7 @@ func GetMessage(id string) (*data.Message, error) {
|
||||
// strip base tags
|
||||
var re = regexp.MustCompile(`(?U)<base .*>`)
|
||||
html = re.ReplaceAllString(html, "")
|
||||
obj.HTML = html
|
||||
|
||||
for _, i := range env.Inlines {
|
||||
if i.FileName != "" || i.ContentID != "" {
|
||||
@ -407,8 +407,6 @@ func GetMessage(id string) (*data.Message, error) {
|
||||
}
|
||||
}
|
||||
|
||||
obj.HTML = html
|
||||
|
||||
// mark message as read
|
||||
if err := MarkRead(id); err != nil {
|
||||
return &obj, err
|
||||
@ -511,6 +509,7 @@ func MarkAllRead() error {
|
||||
|
||||
_, err := sqlf.Update("mailbox").
|
||||
Set("Read", 1).
|
||||
Where("Read = ?", 0).
|
||||
ExecAndClose(context.Background(), db)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -524,6 +523,29 @@ func MarkAllRead() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkAllUnread will mark all messages as unread
|
||||
func MarkAllUnread() error {
|
||||
var (
|
||||
start = time.Now()
|
||||
total = CountRead()
|
||||
)
|
||||
|
||||
_, err := sqlf.Update("mailbox").
|
||||
Set("Read", 0).
|
||||
Where("Read = ?", 1).
|
||||
ExecAndClose(context.Background(), db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
logger.Log().Debugf("[db] marked %d messages as unread in %s", total, elapsed)
|
||||
|
||||
dbLastAction = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkUnread will mark a message as unread
|
||||
func MarkUnread(id string) error {
|
||||
if IsUnread(id) {
|
||||
@ -655,7 +677,6 @@ func CountTotal() int {
|
||||
}
|
||||
|
||||
// CountUnread returns the number of emails in the database that are unread.
|
||||
// If an ID is supplied, then it is just limited to that message.
|
||||
func CountUnread() int {
|
||||
var total int
|
||||
|
||||
@ -668,6 +689,19 @@ func CountUnread() int {
|
||||
return total
|
||||
}
|
||||
|
||||
// CountRead returns the number of emails in the database that are read.
|
||||
func CountRead() int {
|
||||
var total int
|
||||
|
||||
q := sqlf.From("mailbox").
|
||||
Select("COUNT(*)").To(&total).
|
||||
Where("Read = ?", 1)
|
||||
|
||||
_ = q.QueryRowAndClose(nil, db)
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// IsUnread returns the number of emails in the database that are unread.
|
||||
// If an ID is supplied, then it is just limited to that message.
|
||||
func IsUnread(id string) bool {
|
||||
|
@ -85,7 +85,7 @@ func TestMimeEmailInserts(t *testing.T) {
|
||||
|
||||
assertEqual(t, CountTotal(), 0, "incorrect number of mime emails deleted")
|
||||
|
||||
t.Logf("deleted %d mime emails in %s", testRuns, time.Since(delStart))
|
||||
t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart))
|
||||
|
||||
assertEqualStats(t, 0, 0)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user