diff --git a/go.mod b/go.mod index c50449649..1cc79ace2 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e github.com/gabriel-vasile/mimetype v1.4.12 + github.com/emersion/go-imap/v2 v2.0.0-beta.5 github.com/ggwhite/go-masker v1.1.0 github.com/go-chi/chi/v5 v5.2.4 github.com/go-chi/render v1.0.3 @@ -166,6 +167,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/ceph/go-ceph v0.37.0 // indirect + github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cevaris/ordered_map v0.0.0-20190319150403-3adeae072e73 // indirect github.com/clipperhouse/displaywidth v0.6.2 // indirect @@ -197,8 +199,11 @@ require ( github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc // indirect + github.com/emersion/go-message v0.18.1 // indirect + github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/emvi/iso-639-1 v1.1.1 // indirect github.com/evanphx/json-patch/v5 v5.5.0 // indirect @@ -208,6 +213,8 @@ require ( github.com/gdexlab/go-render v1.0.1 // indirect github.com/go-acme/lego/v4 v4.4.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect + github.com/go-crypt/crypt v0.4.5 // indirect + github.com/go-crypt/x v0.4.7 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-git/v5 v5.13.2 // indirect @@ -227,7 +234,7 @@ require ( github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/go-test/deep v1.1.0 // indirect + github.com/go-test/deep v1.1.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/httphead v0.1.0 // indirect @@ -238,6 +245,7 @@ require ( github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -257,8 +265,10 @@ require ( github.com/huandu/xstrings v1.5.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/imdario/mergo v0.3.15 // indirect + github.com/inbucket/html2text v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jhillyerd/enmime/v2 v2.2.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/juliangruber/go-intersect v1.1.0 // indirect @@ -359,6 +369,8 @@ require ( github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spacewander/go-suffix-tree v0.0.0-20191010040751-0865e368c784 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/studio-b12/gowebdav v0.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect @@ -400,6 +412,7 @@ require ( google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect + gopkg.in/loremipsum.v1 v1.1.2 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/go.sum b/go.sum index c4f438713..363e5ab54 100644 --- a/go.sum +++ b/go.sum @@ -218,6 +218,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/ceph/go-ceph v0.37.0 h1:KXliBe3ZDr3/AtfY7n9d1MG7ippYNCVhMPcAgm05CFI= github.com/ceph/go-ceph v0.37.0/go.mod h1:3y2tOlITlyuVFhy8v6PpCEfjMwKPfXJiH0/2hKZZQRE= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -324,6 +326,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk= +github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e h1:rcHHSQqzCgvlwP0I/fQ8rQMn/MpHE5gWSLdtpxtP6KQ= github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e/go.mod h1:Byz7q8MSzSPkouskHJhX0er2mZY/m0Vj5bMeMCkkyY4= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -335,6 +339,12 @@ github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc h1:6IxmRbXV8WXVkcYcTzk github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc/go.mod h1:FdVN2WHg7zOHhJ7kZQdDorfFhIfqZaHttjAzDDvAXHE= github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA= +github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= +github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= +github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= +github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= +github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emvi/iso-639-1 v1.1.1 h1:7jrl1Sqw9ZYWmCOaH+cpQotLbGr/khwlLPXlBvE8WXU= @@ -392,6 +402,10 @@ github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfup github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= +github.com/go-crypt/crypt v0.4.5 h1:cCR5vVejGk1kurwoGfkLxGORY+Pc9GiE7xKCpyHZ3n4= +github.com/go-crypt/crypt v0.4.5/go.mod h1:cQijpCkqavdF52J1bE0PObWwqKKjQCHASHQ2dtLzOJs= +github.com/go-crypt/x v0.4.7 h1:hObjW67nhq/GI1jaD7XCv5RoiVKzF46XIbULgzH71oU= +github.com/go-crypt/x v0.4.7/go.mod h1:K3q7VmLC0U1QFAPn0SQvXjkAtu6FJuH0rN9LNqobX6k= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= @@ -479,6 +493,7 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= @@ -507,6 +522,8 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= +github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -675,6 +692,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inbucket/html2text v0.9.0 h1:ULJmVcBEMAcmLE+/rN815KG1Fx6+a4HhbUxiDiN+qks= +github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -701,6 +720,8 @@ github.com/jellydator/ttlcache/v2 v2.11.1/go.mod h1:RtE5Snf0/57e+2cLWFYWCCsLas2H github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8= +github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -1179,10 +1200,10 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -1805,6 +1826,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/loremipsum.v1 v1.1.2 h1:12APklfJKuGszqZsrArW5QoQh03/W+qyCCjvnDuS6Tw= +gopkg.in/loremipsum.v1 v1.1.2/go.mod h1:TuRvzFuzuejXj+odBU6Tubp/EPUyGb9wmSvHenyP2Ts= gopkg.in/ns1/ns1-go.v2 v2.4.4/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/pkg/jmap/jmap_integration_test.go b/pkg/jmap/jmap_integration_test.go new file mode 100644 index 000000000..0e60a92a7 --- /dev/null +++ b/pkg/jmap/jmap_integration_test.go @@ -0,0 +1,466 @@ +package jmap + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "log" + "math/rand" + "net" + "net/http" + "net/mail" + "net/url" + "os" + "regexp" + "strings" + "testing" + "text/template" + "time" + + "github.com/jhillyerd/enmime/v2" + "github.com/stretchr/testify/require" + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapclient" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + petname "github.com/dustinkirkland/golang-petname" + pw "github.com/sethvargo/go-password/password" + "gopkg.in/loremipsum.v1" + + clog "github.com/opencloud-eu/opencloud/pkg/log" + + "github.com/go-crypt/crypt/algorithm/shacrypt" +) + +var ( + domains = [...]string{"earth.gov", "mars.mil", "opa.org", "acme.com"} + people = [...]string{ + "Camina Drummer", + "Amos Burton", + "James Holden", + "Anderson Dawes", + "Naomi Nagata", + "Klaes Ashford", + "Fred Johnson", + "Chrisjen Avasarala", + "Bobby Draper", + } +) + +const ( + stalwartImage = "ghcr.io/stalwartlabs/stalwart:v0.13.2-alpine" + httpPort = "8080" + imapsPort = "993" + configTemplate = ` +authentication.fallback-admin.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj." +authentication.fallback-admin.user = "mailadmin" +authentication.master.secret = "{{.masterpassword}}" +authentication.master.user = "{{.masterusername}}" +directory.memory.principals.0000.class = "admin" +directory.memory.principals.0000.description = "Superuser" +directory.memory.principals.0000.email.0000 = "admin@example.org" +directory.memory.principals.0000.name = "admin" +directory.memory.principals.0000.secret = "secret" +directory.memory.principals.0001.class = "individual" +directory.memory.principals.0001.description = "{{.description}}" +directory.memory.principals.0001.email.0000 = "{{.email}}" +directory.memory.principals.0001.name = "{{.username}}" +directory.memory.principals.0001.secret = "{{.password}}" +directory.memory.principals.0001.storage.directory = "memory" +directory.memory.type = "memory" +metrics.prometheus.enable = false +server.listener.http.bind = "[::]:{{.httpPort}}" +server.listener.http.protocol = "http" +server.listener.imaptls.bind = "[::]:{{.imapsPort}}" +server.listener.imaptls.protocol = "imap" +server.listener.imaptls.tls.implicit = true +server.hostname = "{{.hostname}}" +server.max-connections = 8192 +server.socket.backlog = 1024 +server.socket.nodelay = true +server.socket.reuse-addr = true +server.socket.reuse-port = true +storage.blob = "rocksdb" +storage.data = "rocksdb" +storage.directory = "memory" +storage.fts = "rocksdb" +storage.lookup = "rocksdb" +store.rocksdb.compression = "lz4" +store.rocksdb.path = "/opt/stalwart/data" +store.rocksdb.type = "rocksdb" +tracer.log.ansi = false +tracer.log.buffered = false +tracer.log.enable = true +tracer.log.level = "trace" +tracer.log.lossy = false +tracer.log.multiline = false +tracer.log.type = "stdout" +` +) + +func htmlJoin(parts []string) []string { + var result []string + for i := range parts { + result = append(result, fmt.Sprintf("

%v

", parts[i])) + } + return result +} + +var paraSplitter = regexp.MustCompile("[\r\n]+") +var emailSplitter = regexp.MustCompile("(.+)@(.+)$") + +func htmlFormat(body string, msg enmime.MailBuilder) enmime.MailBuilder { + return msg.HTML([]byte(strings.Join(htmlJoin(paraSplitter.Split(body, -1)), "\n"))) +} + +func textFormat(body string, msg enmime.MailBuilder) enmime.MailBuilder { + return msg.Text([]byte(body)) +} + +func bothFormat(body string, msg enmime.MailBuilder) enmime.MailBuilder { + msg = htmlFormat(body, msg) + msg = textFormat(body, msg) + return msg +} + +var formats = []func(string, enmime.MailBuilder) enmime.MailBuilder{ + htmlFormat, + textFormat, + bothFormat, +} + +func fill(require *require.Assertions, i *imapclient.Client, folder string, to string, count int, ccEvery int, bccEvery int) { + address, err := mail.ParseAddress(to) + require.NoError(err) + displayName := address.Name + + addressParts := emailSplitter.FindAllStringSubmatch(address.Address, 3) + require.Len(addressParts, 1) + require.Len(addressParts[0], 3) + domain := addressParts[0][2] + + toName := displayName + toAddress := to + ccName1 := "Team Lead" + ccAddress1 := fmt.Sprintf("lead@%s", domain) + ccName2 := "Coworker" + ccAddress2 := fmt.Sprintf("coworker@%s", domain) + bccName := "HR" + bccAddress := fmt.Sprintf("corporate@%s", domain) + titler := cases.Title(language.English, cases.NoLower) + + loremIpsumGenerator := loremipsum.New() + for n := range count { + first := petname.Adjective() + last := petname.Adverb() + messageId := fmt.Sprintf("%d.%d@%s", time.Now().Unix(), 1000000+rand.Intn(8999999), domain) + + format := formats[n%len(formats)] + + text := loremIpsumGenerator.Paragraphs(2 + rand.Intn(9)) + from := fmt.Sprintf("%s.%s@%s", strings.ToLower(first), strings.ToLower(last), domain) + sender := fmt.Sprintf("%s %s <%s.%s@%s>", titler.String(first), titler.String(last), strings.ToLower(first), strings.ToLower(last), domain) + + msg := enmime.Builder(). + From(titler.String(first)+" "+titler.String(last), from). + Subject(titler.String(loremIpsumGenerator.Words(3+rand.Intn(7)))). + Header("Message-ID", messageId). + Header("Sender", sender). + To(toName, toAddress) + + if n%ccEvery == 0 { + msg = msg.CCAddrs([]mail.Address{{Name: ccName1, Address: ccAddress1}, {Name: ccName2, Address: ccAddress2}}) + } + if n%bccEvery == 0 { + msg = msg.BCC(bccName, bccAddress) + } + + msg = format(text, msg) + + buf := new(bytes.Buffer) + part, _ := msg.Build() + part.Encode(buf) + mail := buf.String() + + size := int64(len(mail)) + appendCmd := i.Append(folder, size, nil) + _, err := appendCmd.Write([]byte(mail)) + require.NoError(err) + err = appendCmd.Close() + require.NoError(err) + _, err = appendCmd.Wait() + require.NoError(err) + } +} + +func mailboxId(role string, mailboxes []Mailbox) string { + for _, m := range mailboxes { + if m.Role == role { + return m.Id + } + } + return "" +} + +func skip(t *testing.T) bool { + if os.Getenv("CI") == "woodpecker" { + t.Skip("Skipping tests because CI==wookpecker") + return true + } + if os.Getenv("CI_SYSTEM_NAME") == "woodpecker" { + t.Skip("Skipping tests because CI_SYSTEM_NAME==wookpecker") + return true + } + if os.Getenv("USE_TESTCONTAINERS") == "false" { + t.Skip("Skipping tests because USE_TESTCONTAINERS==false") + return true + } + return false +} + +func TestWithStalwart(t *testing.T) { + if skip(t) { + return + } + require := require.New(t) + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + // A master user name different from "master" does not seem to work as of the current Stalwart version + //masterUsernameSuffix, err := pw.Generate(4+rand.Intn(28), 2, 0, false, true) + //require.NoError(err) + masterUsername := "master" //"master_" + masterUsernameSuffix + + masterPassword, err := pw.Generate(4+rand.Intn(28), 2, 0, false, true) + require.NoError(err) + masterPasswordHash := "" + { + hasher, err := shacrypt.New(shacrypt.WithSHA512(), shacrypt.WithIterations(shacrypt.IterationsDefaultOmitted)) + require.NoError(err) + + digest, err := hasher.Hash(masterPassword) + require.NoError(err) + masterPasswordHash = digest.Encode() + } + + usernameSuffix, err := pw.Generate(8, 2, 0, true, true) + require.NoError(err) + username := "user_" + usernameSuffix + + password, err := pw.Generate(4+rand.Intn(28), 2, 0, false, true) + require.NoError(err) + + hostname := "localhost" + + userPersonName := people[rand.Intn(len(people))] + var userEmail string + { + domain := domains[rand.Intn(len(domains))] + userEmail = strings.Join(strings.Split(cases.Lower(language.English).String(userPersonName), " "), ".") + "@" + domain + } + + configBuf := bytes.NewBufferString("") + template.Must(template.New("").Parse(configTemplate)).Execute(configBuf, map[string]any{ + "hostname": hostname, + "password": password, + "username": username, + "description": userPersonName, + "email": userEmail, + "masterusername": masterUsername, + "masterpassword": masterPasswordHash, + "httpPort": httpPort, + "imapsPort": imapsPort, + }) + config := configBuf.String() + configReader := strings.NewReader(config) + + container, err := testcontainers.Run( + ctx, + stalwartImage, + testcontainers.WithExposedPorts(httpPort+"/tcp", imapsPort+"/tcp"), + testcontainers.WithFiles(testcontainers.ContainerFile{ + Reader: configReader, + ContainerFilePath: "/opt/stalwart/etc/config.toml", + FileMode: 0o700, + }), + testcontainers.WithWaitStrategyAndDeadline( + 30*time.Second, + wait.ForLog(`Network listener started (network.listen-start) listenerId = "imaptls"`), + wait.ForLog(`Network listener started (network.listen-start) listenerId = "http"`), + ), + ) + + defer func() { + testcontainers.CleanupContainer(t, container) + }() + require.NoError(err) + + ip, err := container.Host(ctx) + require.NoError(err) + + port, err := container.MappedPort(ctx, "993") + require.NoError(err) + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + + count := 5 + + loggerImpl := clog.NewLogger() + logger := &loggerImpl + var j Client + var session *Session + { + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.ResponseHeaderTimeout = time.Duration(30 * time.Second) + tr.TLSClientConfig = tlsConfig + jh := *http.DefaultClient + jh.Transport = tr + + jmapPort, err := container.MappedPort(ctx, httpPort) + require.NoError(err) + jmapBaseUrl := url.URL{ + Scheme: "http", + Host: ip + ":" + jmapPort.Port(), + } + + sessionUrl := jmapBaseUrl.JoinPath(".well-known", "jmap") + + api := NewHttpJmapClient( + &jh, + masterUsername, + masterPassword, + nullHttpJmapApiClientEventListener{}, + ) + + j = NewClient(api, api, api) + s, err := j.FetchSession(sessionUrl, username, logger) + require.NoError(err) + // we have to overwrite the hostname in JMAP URL because the container + // will know its name to be a random Docker container identifier, or + // "localhost" as we defined the hostname in the Stalwart configuration, + // and we also need to overwrite the port number as its not mapped + s.JmapUrl.Host = jmapBaseUrl.Host + session = &s + } + + accountId := session.PrimaryAccounts.Mail + + var inboxFolder string + var inboxId string + { + resp, sessionState, err := j.GetAllMailboxes(accountId, session, ctx, logger) + require.NoError(err) + require.Equal(session.State, sessionState) + mailboxesNameByRole := map[string]string{} + mailboxesUnreadByRole := map[string]int{} + for _, m := range resp.Mailboxes { + if m.Role != "" { + mailboxesNameByRole[m.Role] = m.Name + mailboxesUnreadByRole[m.Role] = m.UnreadEmails + } + } + require.Contains(mailboxesNameByRole, "inbox") + require.Contains(mailboxesUnreadByRole, "inbox") + require.Zero(mailboxesUnreadByRole["inbox"]) + + inboxId = mailboxId("inbox", resp.Mailboxes) + require.NotEmpty(inboxId) + inboxFolder = mailboxesNameByRole["inbox"] + require.NotEmpty(inboxFolder) + } + + { + c, err := imapclient.DialTLS(net.JoinHostPort(ip, port.Port()), &imapclient.Options{TLSConfig: tlsConfig}) + require.NoError(err) + + defer func(imap *imapclient.Client) { + err := imap.Close() + if err != nil { + log.Fatal(err) + } + }(c) + + err = c.Login(username, password).Wait() + require.NoError(err) + + _, err = c.Select(inboxFolder, nil).Wait() + require.NoError(err) + + fill(require, c, inboxFolder, fmt.Sprintf("%s <%s>", userPersonName, userEmail), count, 2, 3) + + listCmd := c.List("", "%", &imap.ListOptions{ + ReturnStatus: &imap.StatusOptions{ + NumMessages: true, + NumUnseen: true, + }, + }) + countMap := make(map[string]int) + for { + mbox := listCmd.Next() + if mbox == nil { + break + } + countMap[mbox.Mailbox] = int(*mbox.Status.NumMessages) + } + + inboxCount := -1 + for f, i := range countMap { + if strings.Compare(strings.ToLower(f), strings.ToLower(inboxFolder)) == 0 { + inboxCount = i + break + } + } + if inboxCount == -1 { + require.FailNowf("huh", "failed to find folder '%v' via IMAP", inboxFolder) + } + require.Equal(count, inboxCount) + + err = listCmd.Close() + require.NoError(err) + } + + { + { + resp, sessionState, err := j.GetIdentity(accountId, session, ctx, logger) + require.NoError(err) + require.Equal(session.State, sessionState) + require.Len(resp.Identities, 1) + require.Equal(userEmail, resp.Identities[0].Email) + require.Equal(userPersonName, resp.Identities[0].Name) + } + + { + resp, sessionState, err := j.GetAllMailboxes(accountId, session, ctx, logger) + require.NoError(err) + require.Equal(session.State, sessionState) + mailboxesUnreadByRole := map[string]int{} + for _, m := range resp.Mailboxes { + if m.Role != "" { + mailboxesUnreadByRole[m.Role] = m.UnreadEmails + } + } + require.Equal(count, mailboxesUnreadByRole["inbox"]) + } + + { + resp, sessionState, err := j.GetAllEmails(accountId, session, ctx, logger, inboxId, 0, 0, false, 0) + require.NoError(err) + require.Equal(session.State, sessionState) + + require.Len(resp.Emails, count) + for _, e := range resp.Emails { + require.Empty(e.BodyValues) + require.False(e.HasAttachment) + require.NotEmpty(e.Subject) + require.NotEmpty(e.MessageId) + require.NotEmpty(e.Preview) + } + } + } +} diff --git a/vendor/github.com/MicahParks/jwkset/storage.go b/vendor/github.com/MicahParks/jwkset/storage.go index 6321e0455..3a057d242 100644 --- a/vendor/github.com/MicahParks/jwkset/storage.go +++ b/vendor/github.com/MicahParks/jwkset/storage.go @@ -213,7 +213,6 @@ type httpStorage struct { func NewStorageFromHTTP(remoteJWKSetURL string, options HTTPClientStorageOptions) (Storage, error) { if options.Client == nil { options.Client = http.DefaultClient - } else { } if options.Ctx == nil { options.Ctx = context.Background() diff --git a/vendor/github.com/cention-sany/utf7/.travis.yml b/vendor/github.com/cention-sany/utf7/.travis.yml new file mode 100644 index 000000000..f3e306bd8 --- /dev/null +++ b/vendor/github.com/cention-sany/utf7/.travis.yml @@ -0,0 +1,12 @@ +language: go + +go: + - 1.4.2 + - 1.7.4 + - tip + +install: + - go get -v ./... + - go get golang.org/x/text/encoding + - go get golang.org/x/text/transform + diff --git a/vendor/github.com/cention-sany/utf7/LICENSE b/vendor/github.com/cention-sany/utf7/LICENSE new file mode 100644 index 000000000..099f43b33 --- /dev/null +++ b/vendor/github.com/cention-sany/utf7/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2013 The Go-IMAP Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + + * Neither the name of the go-imap project nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/cention-sany/utf7/README.md b/vendor/github.com/cention-sany/utf7/README.md new file mode 100644 index 000000000..27bd77c35 --- /dev/null +++ b/vendor/github.com/cention-sany/utf7/README.md @@ -0,0 +1,2 @@ +# utf7 [![Build Status](https://travis-ci.org/cention-sany/utf7.png?branch=master)](https://travis-ci.org/cention-sany/utf7) [![GoDoc](https://godoc.org/github.com/cention-sany/utf7?status.png)](https://godoc.org/github.com/cention-sany/utf7) [![Exago](https://api.exago.io:443/badge/cov/github.com/cention-sany/utf7)](https://exago.io/project/github.com/cention-sany/utf7) [![Exago](https://api.exago.io:443/badge/rank/github.com/cention-sany/utf7)](https://exago.io/project/github.com/cention-sany/utf7) +RFC 2152 - UTF7 encoding and decoding. diff --git a/vendor/github.com/cention-sany/utf7/utf7.go b/vendor/github.com/cention-sany/utf7/utf7.go new file mode 100644 index 000000000..36f39bba9 --- /dev/null +++ b/vendor/github.com/cention-sany/utf7/utf7.go @@ -0,0 +1,518 @@ +// Copyright 2013 The Go-IMAP Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +This package modified from: +https://github.com/mxk/go-imap/blob/master/imap/utf7.go +https://github.com/mxk/go-imap/blob/master/imap/utf7_test.go +IMAP specification uses modified UTF-7. Following are the differences: + 1) Printable US-ASCII except & (0x20 to 0x25 and 0x27 to 0x7e) MUST represent by themselves. + 2) '&' is used to shift modified BASE64 instead of '+'. + 3) Can NOT use superfluous null shift (&...-&...- should be just &......-). + 4) ',' is used in BASE64 code instead of '/'. + 5) '&' is represented '&-'. You can have many '&-&-&-&-'. + 6) No implicit shift from BASE64 to US-ASCII. All BASE64 must end with '-'. + +Actual UTF-7 specification: +Rule 1: direct characters: 62 alphanumeric characters and 9 symbols: ' ( ) , - . / : ? +Rule 2: optional direct characters: all other printable characters in the range +U+0020–U+007E except ~ \ + and space. Plus sign (+) may be encoded as +- +(special case). Plus sign (+) mean the start of 'modified Base64 encoded UTF-16'. +The end of this block is indicated by any character not in the modified Base64. +If character after modified Base64 is a '-' then it is consumed. + +Example: + "1 + 1 = 2" is encoded as "1 +- 1 +AD0 2" //+AD0 is the '=' sign. + "£1" is encoded as "+AKM-1" //+AKM- is the '£' sign where '-' is consumed. + +A "+" character followed immediately by any character other than members +of modified Base64 or "-" is an ill-formed sequence. Convert to Unicode code +point then apply modified BASE64 (rfc2045) to it. Modified BASE64 do not use +padding instead add extra bits. Lines should never be broken in the middle of +a UTF-7 shifted sequence. Rule 3: Space, tab, carriage return and line feed may +also be represented directly as single ASCII bytes. Further content transfer +encoding may be needed if using in email environment. +*/ +package utf7 + +import ( + "bytes" + "encoding/base64" + "errors" + "io/ioutil" + "unicode/utf16" + "unicode/utf8" + + "golang.org/x/text/encoding" + "golang.org/x/text/transform" +) + +const ( + uRepl = '\uFFFD' // Unicode replacement code point + u7min = 0x20 // Minimum self-representing UTF-7 value + u7max = 0x7E // Maximum self-representing UTF-7 value +) + +// copy from golang.org/x/text/encoding/internal +type simpleEncoding struct { + Decoder transform.Transformer + Encoder transform.Transformer +} + +func (e *simpleEncoding) NewDecoder() *encoding.Decoder { + return &encoding.Decoder{Transformer: e.Decoder} +} + +func (e *simpleEncoding) NewEncoder() *encoding.Encoder { + return &encoding.Encoder{Transformer: e.Encoder} +} + +var ( + UTF7 encoding.Encoding = &simpleEncoding{ + utf7Decoder{}, + utf7Encoder{}, + } +) + +// ErrBadUTF7 is returned to indicate invalid modified UTF-7 encoding. +var ErrBadUTF7 = errors.New("utf7: bad utf-7 encoding") + +// Base64 codec for code points outside of the 0x20-0x7E range. +const modifiedbase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +var u7enc = base64.NewEncoding(modifiedbase64) + +func isModifiedBase64(r byte) bool { + if r >= 'A' && r <= 'Z' { + return true + } else if r >= 'a' && r <= 'z' { + return true + } else if r >= '0' && r <= '9' { + return true + } else if r == '+' || r == '/' { + return true + } + return false + // bs := []byte(modifiedbase64) + // for _, b := range bs { + // if b == r { + // return true + // } + // } + // return false +} + +type utf7Decoder struct { + transform.NopResetter +} + +func (d utf7Decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + var implicit bool + var tmp int + + nd, n := len(dst), len(src) + if n == 0 && !atEOF { + return 0, 0, transform.ErrShortSrc + } + for ; nSrc < n; nSrc++ { + if nDst >= nd { + return nDst, nSrc, transform.ErrShortDst + } + if c := src[nSrc]; ((c < u7min || c > u7max) && + c != '\t' && c != '\r' && c != '\n') || + c == '~' || c == '\\' { + return nDst, nSrc, ErrBadUTF7 // Illegal code point in ASCII mode + } else if c != '+' { + dst[nDst] = c // character can self represent + nDst++ + continue + } + // found '+' + start := nSrc + 1 + tmp = nSrc // nSrc remain pointing to '+', tmp point to end of BASE64 + // Find the end of the Base64 or "+-" segment + implicit = false + for tmp++; tmp < n && src[tmp] != '-'; tmp++ { + if !isModifiedBase64(src[tmp]) { + if tmp == start { + return nDst, tmp, ErrBadUTF7 // '+' next char must modified base64 + } + // implicit shift back to ASCII - no need '-' character + implicit = true + break + } + } + if tmp == start { + if tmp == n { + // did not find '-' sign and '+' is last character + // total nSrc no include '+' + if atEOF { + return nDst, nSrc, ErrBadUTF7 // '+' can not at the end + } + // '+' can not at the end, so get more data + return nDst, nSrc, transform.ErrShortSrc + } + dst[nDst] = '+' // Escape sequence "+-" + nDst++ + } else if tmp == n && !atEOF { + // no end of BASE64 marker and still has data + // probably the marker at next block of data + // so go get more data. + return nDst, nSrc, transform.ErrShortSrc + } else if b := utf7dec(src[start:tmp]); len(b) > 0 { + if len(b)+nDst > nd { + // need more space on dst for the decoded modified BASE64 unicode + // total nSrc no include '+' + return nDst, nSrc, transform.ErrShortDst + } + copy(dst[nDst:], b) // Control or non-ASCII code points in Base64 + nDst += len(b) + if implicit { + if nDst >= nd { + return nDst, tmp, transform.ErrShortDst + } + dst[nDst] = src[tmp] // implicit shift + nDst++ + } + if tmp == n { + return nDst, tmp, nil + } + } else { + return nDst, nSrc, ErrBadUTF7 // bad encoding + } + nSrc = tmp + } + return +} + +type utf7Encoder struct { + transform.NopResetter +} + +func calcExpectedSize(runeSize int) (round int) { + numerator := runeSize * 17 + round = numerator / 12 + remain := numerator % 12 + if remain >= 6 { + round++ + } + return +} + +func (e utf7Encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + var c byte + var b []byte + var endminus, needMoreSrc, needMoreDst, foundASCII, hasRuneStart bool + var tmp, compare, lastRuneStart int + var currentSize, maxRuneStart int + var rn rune + + nd, n := len(dst), len(src) + if n == 0 { + if !atEOF { + return 0, 0, transform.ErrShortSrc + } else { + return 0, 0, nil + } + } + for nSrc = 0; nSrc < n; { + if nDst >= nd { + return nDst, nSrc, transform.ErrShortDst + } + c = src[nSrc] + if canSelf(c) { + nSrc++ + dst[nDst] = c + nDst++ + continue + } else if c == '+' { + if nDst+2 > nd { + return nDst, nSrc, transform.ErrShortDst + } + nSrc++ + dst[nDst], dst[nDst+1] = '+', '-' + nDst += 2 + continue + } + start := nSrc + tmp = nSrc // nSrc still point to first non-ASCII + currentSize = 0 + maxRuneStart = nSrc + needMoreDst = false + if utf8.RuneStart(src[nSrc]) { + hasRuneStart = true + } else { + hasRuneStart = false + } + foundASCII = true + for tmp++; tmp < n && !canSelf(src[tmp]) && src[tmp] != '+'; tmp++ { + // if next printable ASCII code point found the loop stop + if utf8.RuneStart(src[tmp]) { + hasRuneStart = true + lastRuneStart = tmp + rn, _ = utf8.DecodeRune(src[maxRuneStart:tmp]) + if rn >= 0x10000 { + currentSize += 4 + } else { + currentSize += 2 + } + if calcExpectedSize(currentSize)+2 > nd-nDst { + needMoreDst = true + } else { + maxRuneStart = tmp + } + } + } + + // following to adjust tmp to right pointer as now tmp can not + // find any good ending (searching end with no result). Adjustment + // base on another earlier feasible valid rune position. + needMoreSrc = false + if tmp == n { + foundASCII = false + if !atEOF { + if !hasRuneStart { + return nDst, nSrc, transform.ErrShortSrc + } else { + //re-adjust tmp to good position to encode + if !utf8.Valid(src[maxRuneStart:]) { + if maxRuneStart == start { + return nDst, nSrc, transform.ErrShortSrc + } + needMoreSrc = true + tmp = maxRuneStart + } + } + } + } + + endminus = false + if hasRuneStart && !needMoreSrc { + // need check if dst enough buffer for transform + rn, _ = utf8.DecodeRune(src[lastRuneStart:tmp]) + if rn >= 0x10000 { + currentSize += 4 + } else { + currentSize += 2 + } + if calcExpectedSize(currentSize)+2 > nd-nDst { + // can not use tmp value as transofrmed size too + // big for dst + endminus = true + needMoreDst = true + tmp = maxRuneStart + } + } + + b = utf7enc(src[start:tmp]) + if len(b) < 2 || b[0] != '+' { + return nDst, nSrc, ErrBadUTF7 // Illegal code point in ASCII mode + } + + if foundASCII { + // printable ASCII found - check if BASE64 type + if isModifiedBase64(src[tmp]) || src[tmp] == '-' { + endminus = true + } + } else { + endminus = true + } + compare = nDst + len(b) + if endminus { + compare++ + } + if compare > nd { + return nDst, nSrc, transform.ErrShortDst + } + copy(dst[nDst:], b) + nDst += len(b) + if endminus { + dst[nDst] = '-' + nDst++ + } + nSrc = tmp + + if needMoreDst { + return nDst, nSrc, transform.ErrShortDst + } + + if needMoreSrc { + return nDst, nSrc, transform.ErrShortSrc + } + } + return +} + +// UTF7Encode converts a string from UTF-8 encoding to modified UTF-7. This +// encoding is used by the Mailbox International Naming Convention (RFC 3501 +// section 5.1.3). Invalid UTF-8 byte sequences are replaced by the Unicode +// replacement code point (U+FFFD). +func UTF7Encode(s string) string { + return string(UTF7EncodeBytes([]byte(s))) +} + +const ( + setD = iota + setO + setRule3 + setInvalid +) + +// get the set of characters group. +func getSetType(c byte) int { + if (c >= 44 && c <= ':') || c == '?' { + return setD + } else if c == 39 || c == '(' || c == ')' { + return setD + } else if c >= 'A' && c <= 'Z' { + return setD + } else if c >= 'a' && c <= 'z' { + return setD + } else if c == '+' || c == '\\' { + return setInvalid + } else if c > ' ' && c < '~' { + return setO + } else if c == ' ' || c == '\t' || + c == '\r' || c == '\n' { + return setRule3 + } + return setInvalid +} + +// Check if can represent by themselves. +func canSelf(c byte) bool { + t := getSetType(c) + if t == setInvalid { + return false + } + return true +} + +// UTF7EncodeBytes converts a byte slice from UTF-8 encoding to modified UTF-7. +func UTF7EncodeBytes(s []byte) []byte { + input := bytes.NewReader(s) + reader := transform.NewReader(input, UTF7.NewEncoder()) + output, err := ioutil.ReadAll(reader) + if err != nil { + return nil + } + return output +} + +// utf7enc converts string s from UTF-8 to UTF-16-BE, encodes the result as +// Base64, removes the padding, and adds UTF-7 shifts. +func utf7enc(s []byte) []byte { + // len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no + // control code points (see table below). + b := make([]byte, 0, len(s)+4) + for len(s) > 0 { + r, size := utf8.DecodeRune(s) + if r > utf8.MaxRune { + r, size = utf8.RuneError, 1 // Bug fix (issue 3785) + } + s = s[size:] + if r1, r2 := utf16.EncodeRune(r); r1 != uRepl { + //log.Println("surrogate triggered") + b = append(b, byte(r1>>8), byte(r1)) + r = r2 + } + b = append(b, byte(r>>8), byte(r)) + } + + // Encode as Base64 + //n := u7enc.EncodedLen(len(b)) + 2 // plus 2 for prefix '+' and suffix '-' + n := u7enc.EncodedLen(len(b)) + 1 // plus for prefix '+' + b64 := make([]byte, n) + u7enc.Encode(b64[1:], b) + + // Strip padding + n -= 2 - (len(b)+2)%3 + b64 = b64[:n] + + // Add UTF-7 shifts + b64[0] = '+' + //b64[n-1] = '-' + return b64 +} + +// UTF7Decode converts a string from modified UTF-7 encoding to UTF-8. +func UTF7Decode(u string) (s string, err error) { + b, err := UTF7DecodeBytes([]byte(u)) + s = string(b) + return +} + +// UTF7DecodeBytes converts a byte slice from modified UTF-7 encoding to UTF-8. +func UTF7DecodeBytes(u []byte) ([]byte, error) { + input := bytes.NewReader([]byte(u)) + reader := transform.NewReader(input, UTF7.NewDecoder()) + output, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + return output, nil +} + +// utf7dec extracts UTF-16-BE bytes from Base64 data and converts them to UTF-8. +// A nil slice is returned if the encoding is invalid. +func utf7dec(b64 []byte) []byte { + var b []byte + + // Allocate a single block of memory large enough to store the Base64 data + // (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes. + // Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence, + // double the space allocation for UTF-8. + if n := len(b64); b64[n-1] == '=' { + return nil + } else if n&3 == 0 { + b = make([]byte, u7enc.DecodedLen(n)*3) + } else { + n += 4 - n&3 + b = make([]byte, n+u7enc.DecodedLen(n)*3) + copy(b[copy(b, b64):n], []byte("==")) + b64, b = b[:n], b[n:] + } + + // Decode Base64 into the first 1/3rd of b + n, err := u7enc.Decode(b, b64) + if err != nil || n&1 == 1 { + return nil + } + + // Decode UTF-16-BE into the remaining 2/3rds of b + b, s := b[:n], b[n:] + j := 0 + for i := 0; i < n; i += 2 { + r := rune(b[i])<<8 | rune(b[i+1]) + if utf16.IsSurrogate(r) { + if i += 2; i == n { + //log.Println("surrogate error1!") + return nil + } + r2 := rune(b[i])<<8 | rune(b[i+1]) + //log.Printf("surrogate! 0x%04X 0x%04X\n", r, r2) + if r = utf16.DecodeRune(r, r2); r == uRepl { + return nil + } + } + j += utf8.EncodeRune(s[j:], r) + } + return s[:j] +} + +/* +The following table shows the number of bytes required to encode each code point +in the specified range using UTF-8 and UTF-16 representations: + ++-----------------+-------+--------+ +| Code points | UTF-8 | UTF-16 | ++-----------------+-------+--------+ +| 000000 - 00007F | 1 | 2 | +| 000080 - 0007FF | 2 | 2 | +| 000800 - 00FFFF | 3 | 2 | +| 010000 - 10FFFF | 4 | 4 | ++-----------------+-------+--------+ + +Source: http://en.wikipedia.org/wiki/Comparison_of_Unicode_encodings +*/ diff --git a/vendor/github.com/dustinkirkland/golang-petname/LICENSE b/vendor/github.com/dustinkirkland/golang-petname/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/github.com/dustinkirkland/golang-petname/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/dustinkirkland/golang-petname/README.md b/vendor/github.com/dustinkirkland/golang-petname/README.md new file mode 100644 index 000000000..2ba4a7837 --- /dev/null +++ b/vendor/github.com/dustinkirkland/golang-petname/README.md @@ -0,0 +1,134 @@ +# petname + +## Name + +**petname** − an [RFC1178](https://tools.ietf.org/html/rfc1178) implementation to generate pronounceable, sometimes even memorable, "pet names", consisting of a random combination of adverbs, an adjective, and an animal name + +## Synopsis + +- Complete version: +``` +usage: petname [-w|--words INT] [-l|--letters INT] [-s|--separator STR] [-d|--dir STR] [-c|--complexity INT] [-u|--ubuntu] +``` + +- Python version: +```bash +usage: petname [-h] [-w WORDS] [-l LETTERS] [-s SEPARATOR] +``` + +## Options +- `-w|--words` number of words in the name, default is 2, +- `-l|--letters` maximum number of letters in each word, default is unlimited, +- `-s|--separator` string used to separate name words, default is `'-'`, +- `-d|--dir` directory containing `adverbs.txt`, `adjectives.txt`, `names.txt`, default is `/usr/share/petname/`, +- `-c|--complexity` [0, 1, 2]; 0 = easy words, 1 = standard words, 2 = complex words, default=1, +- `-u|--ubuntu` generate ubuntu-style names, alliteration of first character of each word. + +## Description + +This utility will generate "pet names", consisting of a random combination of an adverb, adjective, and an animal name. These are useful for unique hostnames or container names, for instance. + +As such, PetName tries to follow the tenets of Zooko’s triangle. Names are: + +- human meaningful +- decentralized +- secure + +Besides this shell utility, there are also native libraries: [python-petname](https://pypi.org/project/petname/), [python3-petname](https://pypi.org/project/petname/), and [golang-petname](https://github.com/dustinkirkland/golang-petname). Here are some programmatic examples in code: + +## Examples + +```bash +$ petname +wiggly-yellowtail + +$ petname --words 1 +robin + +$ petname --words 3 +primly-lasting-toucan + +$ petname --words 4 +angrily-impatiently-sage-longhorn + +$ petname --separator ":" +cool:gobbler + +$ petname --separator "" --words 3 +comparablyheartylionfish + +$ petname --ubuntu +amazed-asp + +$ petname --complexity 0 +massive-colt +``` + +---- + +## Code + +Besides this shell utility, there are also native libraries: python-petname, python3-petname, and golang-petname. Here are some programmatic examples in code: + +### **Golang Example** +Install it with apt: +```bash +$ sudo apt-get install golang-petname +``` + +Or here's an example in golang code: + +```golang +package main + +import ( + "flag" + "fmt" + "math/rand" + "time" + "github.com/dustinkirkland/golang-petname" +) + +var ( + words = flag.Int("words", 2, "The number of words in the pet name") + separator = flag.String("separator", "-", "The separator between words in the pet name") +) + +func init() { + rand.Seed(time.Now().UTC().UnixNano()) +} + +func main() { + flag.Parse() + rand.Seed(time.Now().UnixNano()) + fmt.Println(petname.Generate(*words, *separator)) +} +``` + +### **Python Example** +See: [on pypi](https://pypi.python.org/pypi/petname). + +Install it with [pip](https://pip.pypa.io/): +```bash +$ [sudo] pip install petname +``` + +```python +#!/usr/bin/python +import argparse +import petname +import sys + +parser = argparse.ArgumentParser(description='Generate human readable random names') +parser.add_argument('-w', '--words', help='Number of words in name, default=2', default=2) +parser.add_argument('-l', '--letters', help='Maximum number of letters per word, default=6', default=6) +parser.add_argument('-s', '--separator', help='Separator between words, default="-"', default="-") +parser.options = parser.parse_args() +sys.stdout.write(petname.Generate(int(parser.options.words), parser.options.separator, int(parser.options.letters)) + "\n") +``` + +## Author + +This manpage and the utility were written by Dustin Kirkland <dustin.kirkland@gmail.com> for Ubuntu systems (but may be used by others). Permission is granted to copy, distribute and/or modify this document and the utility under the terms of the Apache2 License. + +The complete text of the Apache2 License can be found in `/usr/share/common-licenses/Apache-2.0` on Debian/Ubuntu systems. diff --git a/vendor/github.com/dustinkirkland/golang-petname/golang-petname.1 b/vendor/github.com/dustinkirkland/golang-petname/golang-petname.1 new file mode 100644 index 000000000..f9fdc7427 --- /dev/null +++ b/vendor/github.com/dustinkirkland/golang-petname/golang-petname.1 @@ -0,0 +1,51 @@ +.TH golang-petname 1 "15 December 2014" golang-petname "golang-petname" +.SH NAME +golang-petname \- utility to generate "pet names", consisting of a random combination of adverbs, an adjective, and a proper name + +.SH SYNOPSIS +\fBgolang-petname\fP [-w|--words INT] [-s|--separator STR] + +.SH OPTIONS + + --words number of words in the name, default is 2 + --separator string used to separate name words, default is '-' + +.SH DESCRIPTION + +This utility will generate "pet names", consisting of a random combination of an adverb, adjective, and proper name. These are useful for unique hostnames, for instance. + +The default packaging contains about 2000 names, 1300 adjectives, and 4000 adverbs, yielding nearly 10 billion unique combinations, covering over 32 bits of unique namespace. + +As such, PetName tries to follow the tenets of Zooko's triangle. Names are: + + - human meaningful + - decentralized + - secure + +.SH EXAMPLES + + $ golang-petname + wiggly-Anna + + $ golang-petname --words 1 + Marco + + $ golang-petname --words 3 + quickly-scornful-Johnathan + + $ golang-petname --words 4 + dolorously-leisurely-wee-Susan + + $ golang-petname --separator ":" + hospitable:Isla + + $ golang-petname --separator "" --words 3 + adeptlystaticNicole + +.SH SEE ALSO +\fIpetname\fP(1) + +.SH AUTHOR +This manpage and the utility were written by Dustin Kirkland for Ubuntu systems (but may be used by others). Permission is granted to copy, distribute and/or modify this document and the utility under the terms of the Apache2 License. + +The complete text of the Apache2 License can be found in \fI/usr/share/common-licenses/Apache-2.0\fP on Debian/Ubuntu systems. diff --git a/vendor/github.com/dustinkirkland/golang-petname/petname.go b/vendor/github.com/dustinkirkland/golang-petname/petname.go new file mode 100644 index 000000000..424733abf --- /dev/null +++ b/vendor/github.com/dustinkirkland/golang-petname/petname.go @@ -0,0 +1,82 @@ +/* + petname: library for generating human-readable, random names + for objects (e.g. hostnames, containers, blobs) + + Copyright 2014 Dustin Kirkland + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package petname is a library for generating human-readable, random +// names for objects (e.g. hostnames, containers, blobs). +package petname + +import ( + "math/rand" + "strings" + "time" +) + +// These lists are autogenerated from the master lists in the project: +// - https://github.com/dustinkirkland/petname +// +// These lists only get modified after updating that branch, and then +// automatically updated by ./debian/update-wordlists.sh as part of +// my release process +var ( + adjectives = [...]string{"able", "above", "absolute", "accepted", "accurate", "ace", "active", "actual", "adapted", "adapting", "adequate", "adjusted", "advanced", "alert", "alive", "allowed", "allowing", "amazed", "amazing", "ample", "amused", "amusing", "apparent", "apt", "arriving", "artistic", "assured", "assuring", "awaited", "awake", "aware", "balanced", "becoming", "beloved", "better", "big", "blessed", "bold", "boss", "brave", "brief", "bright", "bursting", "busy", "calm", "capable", "capital", "careful", "caring", "casual", "causal", "central", "certain", "champion", "charmed", "charming", "cheerful", "chief", "choice", "civil", "classic", "clean", "clear", "clever", "climbing", "close", "closing", "coherent", "comic", "communal", "complete", "composed", "concise", "concrete", "content", "cool", "correct", "cosmic", "crack", "creative", "credible", "crisp", "crucial", "cuddly", "cunning", "curious", "current", "cute", "daring", "darling", "dashing", "dear", "decent", "deciding", "deep", "definite", "delicate", "desired", "destined", "devoted", "direct", "discrete", "distinct", "diverse", "divine", "dominant", "driven", "driving", "dynamic", "eager", "easy", "electric", "elegant", "emerging", "eminent", "enabled", "enabling", "endless", "engaged", "engaging", "enhanced", "enjoyed", "enormous", "enough", "epic", "equal", "equipped", "eternal", "ethical", "evident", "evolved", "evolving", "exact", "excited", "exciting", "exotic", "expert", "factual", "fair", "faithful", "famous", "fancy", "fast", "feasible", "fine", "finer", "firm", "first", "fit", "fitting", "fleet", "flexible", "flowing", "fluent", "flying", "fond", "frank", "free", "fresh", "full", "fun", "funky", "funny", "game", "generous", "gentle", "genuine", "giving", "glad", "glorious", "glowing", "golden", "good", "gorgeous", "grand", "grateful", "great", "growing", "grown", "guided", "guiding", "handy", "happy", "hardy", "harmless", "healthy", "helped", "helpful", "helping", "heroic", "hip", "holy", "honest", "hopeful", "hot", "huge", "humane", "humble", "humorous", "ideal", "immense", "immortal", "immune", "improved", "in", "included", "infinite", "informed", "innocent", "inspired", "integral", "intense", "intent", "internal", "intimate", "inviting", "joint", "just", "keen", "key", "kind", "knowing", "known", "large", "lasting", "leading", "learning", "legal", "legible", "lenient", "liberal", "light", "liked", "literate", "live", "living", "logical", "loved", "loving", "loyal", "lucky", "magical", "magnetic", "main", "major", "many", "massive", "master", "mature", "maximum", "measured", "meet", "merry", "mighty", "mint", "model", "modern", "modest", "moral", "more", "moved", "moving", "musical", "mutual", "national", "native", "natural", "nearby", "neat", "needed", "neutral", "new", "next", "nice", "noble", "normal", "notable", "noted", "novel", "obliging", "on", "one", "open", "optimal", "optimum", "organic", "oriented", "outgoing", "patient", "peaceful", "perfect", "pet", "picked", "pleasant", "pleased", "pleasing", "poetic", "polished", "polite", "popular", "positive", "possible", "powerful", "precious", "precise", "premium", "prepared", "present", "pretty", "primary", "prime", "pro", "probable", "profound", "promoted", "prompt", "proper", "proud", "proven", "pumped", "pure", "quality", "quick", "quiet", "rapid", "rare", "rational", "ready", "real", "refined", "regular", "related", "relative", "relaxed", "relaxing", "relevant", "relieved", "renewed", "renewing", "resolved", "rested", "rich", "right", "robust", "romantic", "ruling", "sacred", "safe", "saved", "saving", "secure", "select", "selected", "sensible", "set", "settled", "settling", "sharing", "sharp", "shining", "simple", "sincere", "singular", "skilled", "smart", "smashing", "smiling", "smooth", "social", "solid", "sought", "sound", "special", "splendid", "square", "stable", "star", "steady", "sterling", "still", "stirred", "stirring", "striking", "strong", "stunning", "subtle", "suitable", "suited", "summary", "sunny", "super", "superb", "supreme", "sure", "sweeping", "sweet", "talented", "teaching", "tender", "thankful", "thorough", "tidy", "tight", "together", "tolerant", "top", "topical", "tops", "touched", "touching", "tough", "true", "trusted", "trusting", "trusty", "ultimate", "unbiased", "uncommon", "unified", "unique", "united", "up", "upright", "upward", "usable", "useful", "valid", "valued", "vast", "verified", "viable", "vital", "vocal", "wanted", "warm", "wealthy", "welcome", "welcomed", "well", "whole", "willing", "winning", "wired", "wise", "witty", "wondrous", "workable", "working", "worthy"} + adverbs = [...]string{"abnormally", "absolutely", "accurately", "actively", "actually", "adequately", "admittedly", "adversely", "allegedly", "amazingly", "annually", "apparently", "arguably", "awfully", "badly", "barely", "basically", "blatantly", "blindly", "briefly", "brightly", "broadly", "carefully", "centrally", "certainly", "cheaply", "cleanly", "clearly", "closely", "commonly", "completely", "constantly", "conversely", "correctly", "curiously", "currently", "daily", "deadly", "deeply", "definitely", "directly", "distinctly", "duly", "eagerly", "early", "easily", "eminently", "endlessly", "enormously", "entirely", "equally", "especially", "evenly", "evidently", "exactly", "explicitly", "externally", "extremely", "factually", "fairly", "finally", "firmly", "firstly", "forcibly", "formally", "formerly", "frankly", "freely", "frequently", "friendly", "fully", "generally", "gently", "genuinely", "ghastly", "gladly", "globally", "gradually", "gratefully", "greatly", "grossly", "happily", "hardly", "heartily", "heavily", "hideously", "highly", "honestly", "hopefully", "hopelessly", "horribly", "hugely", "humbly", "ideally", "illegally", "immensely", "implicitly", "incredibly", "indirectly", "infinitely", "informally", "inherently", "initially", "instantly", "intensely", "internally", "jointly", "jolly", "kindly", "largely", "lately", "legally", "lightly", "likely", "literally", "lively", "locally", "logically", "loosely", "loudly", "lovely", "luckily", "mainly", "manually", "marginally", "mentally", "merely", "mildly", "miserably", "mistakenly", "moderately", "monthly", "morally", "mostly", "multiply", "mutually", "namely", "nationally", "naturally", "nearly", "neatly", "needlessly", "newly", "nicely", "nominally", "normally", "notably", "noticeably", "obviously", "oddly", "officially", "only", "openly", "optionally", "overly", "painfully", "partially", "partly", "perfectly", "personally", "physically", "plainly", "pleasantly", "poorly", "positively", "possibly", "precisely", "preferably", "presently", "presumably", "previously", "primarily", "privately", "probably", "promptly", "properly", "publicly", "purely", "quickly", "quietly", "radically", "randomly", "rapidly", "rarely", "rationally", "readily", "really", "reasonably", "recently", "regularly", "reliably", "remarkably", "remotely", "repeatedly", "rightly", "roughly", "routinely", "sadly", "safely", "scarcely", "secondly", "secretly", "seemingly", "sensibly", "separately", "seriously", "severely", "sharply", "shortly", "similarly", "simply", "sincerely", "singularly", "slightly", "slowly", "smoothly", "socially", "solely", "specially", "steadily", "strangely", "strictly", "strongly", "subtly", "suddenly", "suitably", "supposedly", "surely", "terminally", "terribly", "thankfully", "thoroughly", "tightly", "totally", "trivially", "truly", "typically", "ultimately", "unduly", "uniformly", "uniquely", "unlikely", "urgently", "usefully", "usually", "utterly", "vaguely", "vastly", "verbally", "vertically", "vigorously", "violently", "virtually", "visually", "weekly", "wholly", "widely", "wildly", "willingly", "wrongly", "yearly"} + names = [...]string{"ox", "ant", "ape", "asp", "bat", "bee", "boa", "bug", "cat", "cod", "cow", "cub", "doe", "dog", "eel", "eft", "elf", "elk", "emu", "ewe", "fly", "fox", "gar", "gnu", "hen", "hog", "imp", "jay", "kid", "kit", "koi", "lab", "man", "owl", "pig", "pug", "pup", "ram", "rat", "ray", "yak", "bass", "bear", "bird", "boar", "buck", "bull", "calf", "chow", "clam", "colt", "crab", "crow", "dane", "deer", "dodo", "dory", "dove", "drum", "duck", "fawn", "fish", "flea", "foal", "fowl", "frog", "gnat", "goat", "grub", "gull", "hare", "hawk", "ibex", "joey", "kite", "kiwi", "lamb", "lark", "lion", "loon", "lynx", "mako", "mink", "mite", "mole", "moth", "mule", "mutt", "newt", "orca", "oryx", "pika", "pony", "puma", "seal", "shad", "slug", "sole", "stag", "stud", "swan", "tahr", "teal", "tick", "toad", "tuna", "wasp", "wolf", "worm", "wren", "yeti", "adder", "akita", "alien", "aphid", "bison", "boxer", "bream", "bunny", "burro", "camel", "chimp", "civet", "cobra", "coral", "corgi", "crane", "dingo", "drake", "eagle", "egret", "filly", "finch", "gator", "gecko", "ghost", "ghoul", "goose", "guppy", "heron", "hippo", "horse", "hound", "husky", "hyena", "koala", "krill", "leech", "lemur", "liger", "llama", "louse", "macaw", "midge", "molly", "moose", "moray", "mouse", "panda", "perch", "prawn", "quail", "racer", "raven", "rhino", "robin", "satyr", "shark", "sheep", "shrew", "skink", "skunk", "sloth", "snail", "snake", "snipe", "squid", "stork", "swift", "tapir", "tetra", "tiger", "troll", "trout", "viper", "wahoo", "whale", "zebra", "alpaca", "amoeba", "baboon", "badger", "beagle", "bedbug", "beetle", "bengal", "bobcat", "caiman", "cattle", "cicada", "collie", "condor", "cougar", "coyote", "dassie", "dragon", "earwig", "falcon", "feline", "ferret", "gannet", "gibbon", "glider", "goblin", "gopher", "grouse", "guinea", "hermit", "hornet", "iguana", "impala", "insect", "jackal", "jaguar", "jennet", "kitten", "kodiak", "lizard", "locust", "maggot", "magpie", "mammal", "mantis", "marlin", "marmot", "marten", "martin", "mayfly", "minnow", "monkey", "mullet", "muskox", "ocelot", "oriole", "osprey", "oyster", "parrot", "pigeon", "piglet", "poodle", "possum", "python", "quagga", "rabbit", "raptor", "rodent", "roughy", "salmon", "sawfly", "serval", "shiner", "shrimp", "spider", "sponge", "tarpon", "thrush", "tomcat", "toucan", "turkey", "turtle", "urchin", "vervet", "walrus", "weasel", "weevil", "wombat", "anchovy", "anemone", "bluejay", "buffalo", "bulldog", "buzzard", "caribou", "catfish", "chamois", "cheetah", "chicken", "chigger", "cowbird", "crappie", "crawdad", "cricket", "dogfish", "dolphin", "firefly", "garfish", "gazelle", "gelding", "giraffe", "gobbler", "gorilla", "goshawk", "grackle", "griffon", "grizzly", "grouper", "haddock", "hagfish", "halibut", "hamster", "herring", "javelin", "jawfish", "jaybird", "katydid", "ladybug", "lamprey", "lemming", "leopard", "lioness", "lobster", "macaque", "mallard", "mammoth", "manatee", "mastiff", "meerkat", "mollusk", "monarch", "mongrel", "monitor", "monster", "mudfish", "muskrat", "mustang", "narwhal", "oarfish", "octopus", "opossum", "ostrich", "panther", "peacock", "pegasus", "pelican", "penguin", "phoenix", "piranha", "polecat", "primate", "quetzal", "raccoon", "rattler", "redbird", "redfish", "reptile", "rooster", "sawfish", "sculpin", "seagull", "skylark", "snapper", "spaniel", "sparrow", "sunbeam", "sunbird", "sunfish", "tadpole", "terrier", "unicorn", "vulture", "wallaby", "walleye", "warthog", "whippet", "wildcat", "aardvark", "airedale", "albacore", "anteater", "antelope", "arachnid", "barnacle", "basilisk", "blowfish", "bluebird", "bluegill", "bonefish", "bullfrog", "cardinal", "chipmunk", "cockatoo", "crayfish", "dinosaur", "doberman", "duckling", "elephant", "escargot", "flamingo", "flounder", "foxhound", "glowworm", "goldfish", "grubworm", "hedgehog", "honeybee", "hookworm", "humpback", "kangaroo", "killdeer", "kingfish", "labrador", "lacewing", "ladybird", "lionfish", "longhorn", "mackerel", "malamute", "marmoset", "mastodon", "moccasin", "mongoose", "monkfish", "mosquito", "pangolin", "parakeet", "pheasant", "pipefish", "platypus", "polliwog", "porpoise", "reindeer", "ringtail", "sailfish", "scorpion", "seahorse", "seasnail", "sheepdog", "shepherd", "silkworm", "squirrel", "stallion", "starfish", "starling", "stingray", "stinkbug", "sturgeon", "terrapin", "titmouse", "tortoise", "treefrog", "werewolf", "woodcock"} +) + +// End word lists + +// Call this function once before using any other to get real random results +func NonDeterministicMode() { + rand.Seed(time.Now().UnixNano()) +} + +// Adverb returns a random adverb from a list of petname adverbs. +func Adverb() string { + return adverbs[rand.Intn(len(adverbs))] +} + +// Adjective returns a random adjective from a list of petname adjectives. +func Adjective() string { + return adjectives[rand.Intn(len(adjectives))] +} + +// Name returns a random name from a list of petname names. +func Name() string { + return names[rand.Intn(len(names))] +} + +// Generate generates and returns a random pet name. +// It takes two parameters: the number of words in the name, and a separator token. +// If a single word is requested, simply a Name() is returned. +// If two words are requested, a Adjective() and a Name() are returned. +// If three or more words are requested, a variable number of Adverb() and a Adjective and a Name() is returned. +// The separator can be any character, string, or the empty string. +func Generate(words int, separator string) string { + if words == 1 { + return Name() + } else if words == 2 { + return Adjective() + separator + Name() + } + var petname []string + for i := 0; i < words-2; i++ { + petname = append(petname, Adverb()) + } + petname = append(petname, Adjective(), Name()) + return strings.Join(petname, separator) +} diff --git a/vendor/github.com/emersion/go-imap/v2/.build.yml b/vendor/github.com/emersion/go-imap/v2/.build.yml new file mode 100644 index 000000000..7166ba620 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/.build.yml @@ -0,0 +1,19 @@ +image: alpine/latest +packages: + - dovecot + - go +sources: + - https://github.com/emersion/go-imap#v2 +tasks: + - build: | + cd go-imap + go build -race -v ./... + - test: | + cd go-imap + go test -race ./... + - test-dovecot: | + cd go-imap + GOIMAP_TEST_DOVECOT=1 go test -race ./imapclient + - gofmt: | + cd go-imap + test -z $(gofmt -l .) diff --git a/vendor/github.com/emersion/go-imap/v2/LICENSE b/vendor/github.com/emersion/go-imap/v2/LICENSE new file mode 100644 index 000000000..d6718dcf2 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2013 The Go-IMAP Authors +Copyright (c) 2016 Proton Technologies AG +Copyright (c) 2023 Simon Ser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/emersion/go-imap/v2/README.md b/vendor/github.com/emersion/go-imap/v2/README.md new file mode 100644 index 000000000..c84fdb927 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/README.md @@ -0,0 +1,29 @@ +# go-imap + +[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-imap/v2.svg)](https://pkg.go.dev/github.com/emersion/go-imap/v2) + +An [IMAP4rev2] library for Go. + +> **Note** +> This is the README for go-imap v2. This new major version is still in +> development. For go-imap v1, see the [v1 branch]. + +## Usage + +To add go-imap to your project, run: + + go get github.com/emersion/go-imap/v2 + +Documentation and examples for the module are available here: + +- [Client docs] +- [Server docs] + +## License + +MIT + +[IMAP4rev2]: https://www.rfc-editor.org/rfc/rfc9051.html +[v1 branch]: https://github.com/emersion/go-imap/tree/v1 +[Client docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapclient +[Server docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapserver diff --git a/vendor/github.com/emersion/go-imap/v2/acl.go b/vendor/github.com/emersion/go-imap/v2/acl.go new file mode 100644 index 000000000..4d9431e9d --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/acl.go @@ -0,0 +1,104 @@ +package imap + +import ( + "fmt" + "strings" +) + +// IMAP4 ACL extension (RFC 2086) + +// Right describes a set of operations controlled by the IMAP ACL extension. +type Right byte + +const ( + // Standard rights + RightLookup = Right('l') // mailbox is visible to LIST/LSUB commands + RightRead = Right('r') // SELECT the mailbox, perform CHECK, FETCH, PARTIAL, SEARCH, COPY from mailbox + RightSeen = Right('s') // keep seen/unseen information across sessions (STORE SEEN flag) + RightWrite = Right('w') // STORE flags other than SEEN and DELETED + RightInsert = Right('i') // perform APPEND, COPY into mailbox + RightPost = Right('p') // send mail to submission address for mailbox, not enforced by IMAP4 itself + RightCreate = Right('c') // CREATE new sub-mailboxes in any implementation-defined hierarchy + RightDelete = Right('d') // STORE DELETED flag, perform EXPUNGE + RightAdminister = Right('a') // perform SETACL +) + +// RightSetAll contains all standard rights. +var RightSetAll = RightSet("lrswipcda") + +// RightsIdentifier is an ACL identifier. +type RightsIdentifier string + +// RightsIdentifierAnyone is the universal identity (matches everyone). +const RightsIdentifierAnyone = RightsIdentifier("anyone") + +// NewRightsIdentifierUsername returns a rights identifier referring to a +// username, checking for reserved values. +func NewRightsIdentifierUsername(username string) (RightsIdentifier, error) { + if username == string(RightsIdentifierAnyone) || strings.HasPrefix(username, "-") { + return "", fmt.Errorf("imap: reserved rights identifier") + } + return RightsIdentifier(username), nil +} + +// RightModification indicates how to mutate a right set. +type RightModification byte + +const ( + RightModificationReplace = RightModification(0) + RightModificationAdd = RightModification('+') + RightModificationRemove = RightModification('-') +) + +// A RightSet is a set of rights. +type RightSet []Right + +// String returns a string representation of the right set. +func (r RightSet) String() string { + return string(r) +} + +// Add returns a new right set containing rights from both sets. +func (r RightSet) Add(rights RightSet) RightSet { + newRights := make(RightSet, len(r), len(r)+len(rights)) + copy(newRights, r) + + for _, right := range rights { + if !strings.ContainsRune(string(r), rune(right)) { + newRights = append(newRights, right) + } + } + + return newRights +} + +// Remove returns a new right set containing all rights in r except these in +// the provided set. +func (r RightSet) Remove(rights RightSet) RightSet { + newRights := make(RightSet, 0, len(r)) + + for _, right := range r { + if !strings.ContainsRune(string(rights), rune(right)) { + newRights = append(newRights, right) + } + } + + return newRights +} + +// Equal returns true if both right sets contain exactly the same rights. +func (rs1 RightSet) Equal(rs2 RightSet) bool { + for _, r := range rs1 { + if !strings.ContainsRune(string(rs2), rune(r)) { + return false + } + } + + for _, r := range rs2 { + if !strings.ContainsRune(string(rs1), rune(r)) { + return false + } + } + + return true +} diff --git a/vendor/github.com/emersion/go-imap/v2/append.go b/vendor/github.com/emersion/go-imap/v2/append.go new file mode 100644 index 000000000..13d887fba --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/append.go @@ -0,0 +1,18 @@ +package imap + +import ( + "time" +) + +// AppendOptions contains options for the APPEND command. +type AppendOptions struct { + Flags []Flag + Time time.Time +} + +// AppendData is the data returned by an APPEND command. +type AppendData struct { + // requires UIDPLUS or IMAP4rev2 + UID UID + UIDValidity uint32 +} diff --git a/vendor/github.com/emersion/go-imap/v2/capability.go b/vendor/github.com/emersion/go-imap/v2/capability.go new file mode 100644 index 000000000..0b84c5f24 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/capability.go @@ -0,0 +1,205 @@ +package imap + +import ( + "strconv" + "strings" +) + +// Cap represents an IMAP capability. +type Cap string + +// Registered capabilities. +// +// See: https://www.iana.org/assignments/imap-capabilities/ +const ( + CapIMAP4rev1 Cap = "IMAP4rev1" // RFC 3501 + CapIMAP4rev2 Cap = "IMAP4rev2" // RFC 9051 + + CapAuthPlain Cap = "AUTH=PLAIN" + + CapStartTLS Cap = "STARTTLS" + CapLoginDisabled Cap = "LOGINDISABLED" + + // Folded in IMAP4rev2 + CapNamespace Cap = "NAMESPACE" // RFC 2342 + CapUnselect Cap = "UNSELECT" // RFC 3691 + CapUIDPlus Cap = "UIDPLUS" // RFC 4315 + CapESearch Cap = "ESEARCH" // RFC 4731 + CapSearchRes Cap = "SEARCHRES" // RFC 5182 + CapEnable Cap = "ENABLE" // RFC 5161 + CapIdle Cap = "IDLE" // RFC 2177 + CapSASLIR Cap = "SASL-IR" // RFC 4959 + CapListExtended Cap = "LIST-EXTENDED" // RFC 5258 + CapListStatus Cap = "LIST-STATUS" // RFC 5819 + CapMove Cap = "MOVE" // RFC 6851 + CapLiteralMinus Cap = "LITERAL-" // RFC 7888 + CapStatusSize Cap = "STATUS=SIZE" // RFC 8438 + + CapACL Cap = "ACL" // RFC 4314 + CapAppendLimit Cap = "APPENDLIMIT" // RFC 7889 + CapBinary Cap = "BINARY" // RFC 3516 + CapCatenate Cap = "CATENATE" // RFC 4469 + CapChildren Cap = "CHILDREN" // RFC 3348 + CapCondStore Cap = "CONDSTORE" // RFC 7162 + CapConvert Cap = "CONVERT" // RFC 5259 + CapCreateSpecialUse Cap = "CREATE-SPECIAL-USE" // RFC 6154 + CapESort Cap = "ESORT" // RFC 5267 + CapFilters Cap = "FILTERS" // RFC 5466 + CapID Cap = "ID" // RFC 2971 + CapLanguage Cap = "LANGUAGE" // RFC 5255 + CapListMyRights Cap = "LIST-MYRIGHTS" // RFC 8440 + CapLiteralPlus Cap = "LITERAL+" // RFC 7888 + CapLoginReferrals Cap = "LOGIN-REFERRALS" // RFC 2221 + CapMailboxReferrals Cap = "MAILBOX-REFERRALS" // RFC 2193 + CapMetadata Cap = "METADATA" // RFC 5464 + CapMetadataServer Cap = "METADATA-SERVER" // RFC 5464 + CapMultiAppend Cap = "MULTIAPPEND" // RFC 3502 + CapMultiSearch Cap = "MULTISEARCH" // RFC 7377 + CapNotify Cap = "NOTIFY" // RFC 5465 + CapObjectID Cap = "OBJECTID" // RFC 8474 + CapPreview Cap = "PREVIEW" // RFC 8970 + CapQResync Cap = "QRESYNC" // RFC 7162 + CapQuota Cap = "QUOTA" // RFC 9208 + CapQuotaSet Cap = "QUOTASET" // RFC 9208 + CapReplace Cap = "REPLACE" // RFC 8508 + CapSaveDate Cap = "SAVEDATE" // RFC 8514 + CapSearchFuzzy Cap = "SEARCH=FUZZY" // RFC 6203 + CapSort Cap = "SORT" // RFC 5256 + CapSortDisplay Cap = "SORT=DISPLAY" // RFC 5957 + CapSpecialUse Cap = "SPECIAL-USE" // RFC 6154 + CapUnauthenticate Cap = "UNAUTHENTICATE" // RFC 8437 + CapURLPartial Cap = "URL-PARTIAL" // RFC 5550 + CapURLAuth Cap = "URLAUTH" // RFC 4467 + CapUTF8Accept Cap = "UTF8=ACCEPT" // RFC 6855 + CapUTF8Only Cap = "UTF8=ONLY" // RFC 6855 + CapWithin Cap = "WITHIN" // RFC 5032 + CapUIDOnly Cap = "UIDONLY" // RFC 9586 + CapListMetadata Cap = "LIST-METADATA" // RFC 9590 + CapInProgress Cap = "INPROGRESS" // RFC 9585 +) + +var imap4rev2Caps = CapSet{ + CapNamespace: {}, + CapUnselect: {}, + CapUIDPlus: {}, + CapESearch: {}, + CapSearchRes: {}, + CapEnable: {}, + CapIdle: {}, + CapSASLIR: {}, + CapListExtended: {}, + CapListStatus: {}, + CapMove: {}, + CapLiteralMinus: {}, + CapStatusSize: {}, +} + +// AuthCap returns the capability name for an SASL authentication mechanism. +func AuthCap(mechanism string) Cap { + return Cap("AUTH=" + mechanism) +} + +// CapSet is a set of capabilities. +type CapSet map[Cap]struct{} + +func (set CapSet) has(c Cap) bool { + _, ok := set[c] + return ok +} + +// Has checks whether a capability is supported. +// +// Some capabilities are implied by others, as such Has may return true even if +// the capability is not in the map. +func (set CapSet) Has(c Cap) bool { + if set.has(c) { + return true + } + + if set.has(CapIMAP4rev2) && imap4rev2Caps.has(c) { + return true + } + + if c == CapLiteralMinus && set.has(CapLiteralPlus) { + return true + } + if c == CapCondStore && set.has(CapQResync) { + return true + } + if c == CapUTF8Accept && set.has(CapUTF8Only) { + return true + } + if c == CapAppendLimit { + _, ok := set.AppendLimit() + return ok + } + + return false +} + +// AuthMechanisms returns the list of supported SASL mechanisms for +// authentication. +func (set CapSet) AuthMechanisms() []string { + var l []string + for c := range set { + if !strings.HasPrefix(string(c), "AUTH=") { + continue + } + mech := strings.TrimPrefix(string(c), "AUTH=") + l = append(l, mech) + } + return l +} + +// AppendLimit checks the APPENDLIMIT capability. +// +// If the server supports APPENDLIMIT, ok is true. If the server doesn't have +// the same upload limit for all mailboxes, limit is nil and per-mailbox +// limits must be queried via STATUS. +func (set CapSet) AppendLimit() (limit *uint32, ok bool) { + if set.has(CapAppendLimit) { + return nil, true + } + + for c := range set { + if !strings.HasPrefix(string(c), "APPENDLIMIT=") { + continue + } + + limitStr := strings.TrimPrefix(string(c), "APPENDLIMIT=") + limit64, err := strconv.ParseUint(limitStr, 10, 32) + if err == nil && limit64 > 0 { + limit32 := uint32(limit64) + return &limit32, true + } + } + + limit32 := ^uint32(0) + return &limit32, false +} + +// QuotaResourceTypes returns the list of supported QUOTA resource types. +func (set CapSet) QuotaResourceTypes() []QuotaResourceType { + var l []QuotaResourceType + for c := range set { + if !strings.HasPrefix(string(c), "QUOTA=RES-") { + continue + } + t := strings.TrimPrefix(string(c), "QUOTA=RES-") + l = append(l, QuotaResourceType(t)) + } + return l +} + +// ThreadAlgorithms returns the list of supported threading algorithms. +func (set CapSet) ThreadAlgorithms() []ThreadAlgorithm { + var l []ThreadAlgorithm + for c := range set { + if !strings.HasPrefix(string(c), "THREAD=") { + continue + } + alg := strings.TrimPrefix(string(c), "THREAD=") + l = append(l, ThreadAlgorithm(alg)) + } + return l +} diff --git a/vendor/github.com/emersion/go-imap/v2/copy.go b/vendor/github.com/emersion/go-imap/v2/copy.go new file mode 100644 index 000000000..f685a6092 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/copy.go @@ -0,0 +1,9 @@ +package imap + +// CopyData is the data returned by a COPY command. +type CopyData struct { + // requires UIDPLUS or IMAP4rev2 + UIDValidity uint32 + SourceUIDs UIDSet + DestUIDs UIDSet +} diff --git a/vendor/github.com/emersion/go-imap/v2/create.go b/vendor/github.com/emersion/go-imap/v2/create.go new file mode 100644 index 000000000..09e8bc4a7 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/create.go @@ -0,0 +1,6 @@ +package imap + +// CreateOptions contains options for the CREATE command. +type CreateOptions struct { + SpecialUse []MailboxAttr // requires CREATE-SPECIAL-USE +} diff --git a/vendor/github.com/emersion/go-imap/v2/fetch.go b/vendor/github.com/emersion/go-imap/v2/fetch.go new file mode 100644 index 000000000..f146c8971 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/fetch.go @@ -0,0 +1,284 @@ +package imap + +import ( + "fmt" + "strings" + "time" +) + +// FetchOptions contains options for the FETCH command. +type FetchOptions struct { + // Fields to fetch + BodyStructure *FetchItemBodyStructure + Envelope bool + Flags bool + InternalDate bool + RFC822Size bool + UID bool + BodySection []*FetchItemBodySection + BinarySection []*FetchItemBinarySection // requires IMAP4rev2 or BINARY + BinarySectionSize []*FetchItemBinarySectionSize // requires IMAP4rev2 or BINARY + ModSeq bool // requires CONDSTORE + + ChangedSince uint64 // requires CONDSTORE +} + +// FetchItemBodyStructure contains FETCH options for the body structure. +type FetchItemBodyStructure struct { + Extended bool +} + +// PartSpecifier describes whether to fetch a part's header, body, or both. +type PartSpecifier string + +const ( + PartSpecifierNone PartSpecifier = "" + PartSpecifierHeader PartSpecifier = "HEADER" + PartSpecifierMIME PartSpecifier = "MIME" + PartSpecifierText PartSpecifier = "TEXT" +) + +// SectionPartial describes a byte range when fetching a message's payload. +type SectionPartial struct { + Offset, Size int64 +} + +// FetchItemBodySection is a FETCH BODY[] data item. +// +// To fetch the whole body of a message, use the zero FetchItemBodySection: +// +// imap.FetchItemBodySection{} +// +// To fetch only a specific part, use the Part field: +// +// imap.FetchItemBodySection{Part: []int{1, 2, 3}} +// +// To fetch only the header of the message, use the Specifier field: +// +// imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader} +type FetchItemBodySection struct { + Specifier PartSpecifier + Part []int + HeaderFields []string + HeaderFieldsNot []string + Partial *SectionPartial + Peek bool +} + +// FetchItemBinarySection is a FETCH BINARY[] data item. +type FetchItemBinarySection struct { + Part []int + Partial *SectionPartial + Peek bool +} + +// FetchItemBinarySectionSize is a FETCH BINARY.SIZE[] data item. +type FetchItemBinarySectionSize struct { + Part []int +} + +// Envelope is the envelope structure of a message. +// +// The subject and addresses are UTF-8 (ie, not in their encoded form). The +// In-Reply-To and Message-ID values contain message identifiers without angle +// brackets. +type Envelope struct { + Date time.Time + Subject string + From []Address + Sender []Address + ReplyTo []Address + To []Address + Cc []Address + Bcc []Address + InReplyTo []string + MessageID string +} + +// Address represents a sender or recipient of a message. +type Address struct { + Name string + Mailbox string + Host string +} + +// Addr returns the e-mail address in the form "foo@example.org". +// +// If the address is a start or end of group, the empty string is returned. +func (addr *Address) Addr() string { + if addr.Mailbox == "" || addr.Host == "" { + return "" + } + return addr.Mailbox + "@" + addr.Host +} + +// IsGroupStart returns true if this address is a start of group marker. +// +// In that case, Mailbox contains the group name phrase. +func (addr *Address) IsGroupStart() bool { + return addr.Host == "" && addr.Mailbox != "" +} + +// IsGroupEnd returns true if this address is a end of group marker. +func (addr *Address) IsGroupEnd() bool { + return addr.Host == "" && addr.Mailbox == "" +} + +// BodyStructure describes the body structure of a message. +// +// A BodyStructure value is either a *BodyStructureSinglePart or a +// *BodyStructureMultiPart. +type BodyStructure interface { + // MediaType returns the MIME type of this body structure, e.g. "text/plain". + MediaType() string + // Walk walks the body structure tree, calling f for each part in the tree, + // including bs itself. The parts are visited in DFS pre-order. + Walk(f BodyStructureWalkFunc) + // Disposition returns the body structure disposition, if available. + Disposition() *BodyStructureDisposition + + bodyStructure() +} + +var ( + _ BodyStructure = (*BodyStructureSinglePart)(nil) + _ BodyStructure = (*BodyStructureMultiPart)(nil) +) + +// BodyStructureSinglePart is a body structure with a single part. +type BodyStructureSinglePart struct { + Type, Subtype string + Params map[string]string + ID string + Description string + Encoding string + Size uint32 + + MessageRFC822 *BodyStructureMessageRFC822 // only for "message/rfc822" + Text *BodyStructureText // only for "text/*" + Extended *BodyStructureSinglePartExt +} + +func (bs *BodyStructureSinglePart) MediaType() string { + return strings.ToLower(bs.Type) + "/" + strings.ToLower(bs.Subtype) +} + +func (bs *BodyStructureSinglePart) Walk(f BodyStructureWalkFunc) { + f([]int{1}, bs) +} + +func (bs *BodyStructureSinglePart) Disposition() *BodyStructureDisposition { + if bs.Extended == nil { + return nil + } + return bs.Extended.Disposition +} + +// Filename decodes the body structure's filename, if any. +func (bs *BodyStructureSinglePart) Filename() string { + var filename string + if bs.Extended != nil && bs.Extended.Disposition != nil { + filename = bs.Extended.Disposition.Params["filename"] + } + if filename == "" { + // Note: using "name" in Content-Type is discouraged + filename = bs.Params["name"] + } + return filename +} + +func (*BodyStructureSinglePart) bodyStructure() {} + +// BodyStructureMessageRFC822 contains metadata specific to RFC 822 parts for +// BodyStructureSinglePart. +type BodyStructureMessageRFC822 struct { + Envelope *Envelope + BodyStructure BodyStructure + NumLines int64 +} + +// BodyStructureText contains metadata specific to text parts for +// BodyStructureSinglePart. +type BodyStructureText struct { + NumLines int64 +} + +// BodyStructureSinglePartExt contains extended body structure data for +// BodyStructureSinglePart. +type BodyStructureSinglePartExt struct { + Disposition *BodyStructureDisposition + Language []string + Location string +} + +// BodyStructureMultiPart is a body structure with multiple parts. +type BodyStructureMultiPart struct { + Children []BodyStructure + Subtype string + + Extended *BodyStructureMultiPartExt +} + +func (bs *BodyStructureMultiPart) MediaType() string { + return "multipart/" + strings.ToLower(bs.Subtype) +} + +func (bs *BodyStructureMultiPart) Walk(f BodyStructureWalkFunc) { + bs.walk(f, nil) +} + +func (bs *BodyStructureMultiPart) walk(f BodyStructureWalkFunc, path []int) { + if !f(path, bs) { + return + } + + pathBuf := make([]int, len(path)) + copy(pathBuf, path) + for i, part := range bs.Children { + num := i + 1 + partPath := append(pathBuf, num) + + switch part := part.(type) { + case *BodyStructureSinglePart: + f(partPath, part) + case *BodyStructureMultiPart: + part.walk(f, partPath) + default: + panic(fmt.Errorf("unsupported body structure type %T", part)) + } + } +} + +func (bs *BodyStructureMultiPart) Disposition() *BodyStructureDisposition { + if bs.Extended == nil { + return nil + } + return bs.Extended.Disposition +} + +func (*BodyStructureMultiPart) bodyStructure() {} + +// BodyStructureMultiPartExt contains extended body structure data for +// BodyStructureMultiPart. +type BodyStructureMultiPartExt struct { + Params map[string]string + Disposition *BodyStructureDisposition + Language []string + Location string +} + +// BodyStructureDisposition describes the content disposition of a part +// (specified in the Content-Disposition header field). +type BodyStructureDisposition struct { + Value string + Params map[string]string +} + +// BodyStructureWalkFunc is a function called for each body structure visited +// by BodyStructure.Walk. +// +// The path argument contains the IMAP part path. +// +// The function should return true to visit all of the part's children or false +// to skip them. +type BodyStructureWalkFunc func(path []int, part BodyStructure) (walkChildren bool) diff --git a/vendor/github.com/emersion/go-imap/v2/id.go b/vendor/github.com/emersion/go-imap/v2/id.go new file mode 100644 index 000000000..de7ca0e12 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/id.go @@ -0,0 +1,15 @@ +package imap + +type IDData struct { + Name string + Version string + OS string + OSVersion string + Vendor string + SupportURL string + Address string + Date string + Command string + Arguments string + Environment string +} diff --git a/vendor/github.com/emersion/go-imap/v2/imap.go b/vendor/github.com/emersion/go-imap/v2/imap.go new file mode 100644 index 000000000..7b4335718 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imap.go @@ -0,0 +1,105 @@ +// Package imap implements IMAP4rev2. +// +// IMAP4rev2 is defined in RFC 9051. +// +// This package contains types and functions common to both the client and +// server. See the imapclient and imapserver sub-packages. +package imap + +import ( + "fmt" + "io" +) + +// ConnState describes the connection state. +// +// See RFC 9051 section 3. +type ConnState int + +const ( + ConnStateNone ConnState = iota + ConnStateNotAuthenticated + ConnStateAuthenticated + ConnStateSelected + ConnStateLogout +) + +// String implements fmt.Stringer. +func (state ConnState) String() string { + switch state { + case ConnStateNone: + return "none" + case ConnStateNotAuthenticated: + return "not authenticated" + case ConnStateAuthenticated: + return "authenticated" + case ConnStateSelected: + return "selected" + case ConnStateLogout: + return "logout" + default: + panic(fmt.Errorf("imap: unknown connection state %v", int(state))) + } +} + +// MailboxAttr is a mailbox attribute. +// +// Mailbox attributes are defined in RFC 9051 section 7.3.1. +type MailboxAttr string + +const ( + // Base attributes + MailboxAttrNonExistent MailboxAttr = "\\NonExistent" + MailboxAttrNoInferiors MailboxAttr = "\\Noinferiors" + MailboxAttrNoSelect MailboxAttr = "\\Noselect" + MailboxAttrHasChildren MailboxAttr = "\\HasChildren" + MailboxAttrHasNoChildren MailboxAttr = "\\HasNoChildren" + MailboxAttrMarked MailboxAttr = "\\Marked" + MailboxAttrUnmarked MailboxAttr = "\\Unmarked" + MailboxAttrSubscribed MailboxAttr = "\\Subscribed" + MailboxAttrRemote MailboxAttr = "\\Remote" + + // Role (aka. "special-use") attributes + MailboxAttrAll MailboxAttr = "\\All" + MailboxAttrArchive MailboxAttr = "\\Archive" + MailboxAttrDrafts MailboxAttr = "\\Drafts" + MailboxAttrFlagged MailboxAttr = "\\Flagged" + MailboxAttrJunk MailboxAttr = "\\Junk" + MailboxAttrSent MailboxAttr = "\\Sent" + MailboxAttrTrash MailboxAttr = "\\Trash" + MailboxAttrImportant MailboxAttr = "\\Important" // RFC 8457 +) + +// Flag is a message flag. +// +// Message flags are defined in RFC 9051 section 2.3.2. +type Flag string + +const ( + // System flags + FlagSeen Flag = "\\Seen" + FlagAnswered Flag = "\\Answered" + FlagFlagged Flag = "\\Flagged" + FlagDeleted Flag = "\\Deleted" + FlagDraft Flag = "\\Draft" + + // Widely used flags + FlagForwarded Flag = "$Forwarded" + FlagMDNSent Flag = "$MDNSent" // Message Disposition Notification sent + FlagJunk Flag = "$Junk" + FlagNotJunk Flag = "$NotJunk" + FlagPhishing Flag = "$Phishing" + FlagImportant Flag = "$Important" // RFC 8457 + + // Permanent flags + FlagWildcard Flag = "\\*" +) + +// LiteralReader is a reader for IMAP literals. +type LiteralReader interface { + io.Reader + Size() int64 +} + +// UID is a message unique identifier. +type UID uint32 diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/acl.go b/vendor/github.com/emersion/go-imap/v2/imapclient/acl.go new file mode 100644 index 000000000..b20be3b79 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/acl.go @@ -0,0 +1,138 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// MyRights sends a MYRIGHTS command. +// +// This command requires support for the ACL extension. +func (c *Client) MyRights(mailbox string) *MyRightsCommand { + cmd := &MyRightsCommand{} + enc := c.beginCommand("MYRIGHTS", cmd) + enc.SP().Mailbox(mailbox) + enc.end() + return cmd +} + +// SetACL sends a SETACL command. +// +// This command requires support for the ACL extension. +func (c *Client) SetACL(mailbox string, ri imap.RightsIdentifier, rm imap.RightModification, rs imap.RightSet) *SetACLCommand { + cmd := &SetACLCommand{} + enc := c.beginCommand("SETACL", cmd) + enc.SP().Mailbox(mailbox).SP().String(string(ri)).SP() + enc.String(internal.FormatRights(rm, rs)) + enc.end() + return cmd +} + +// SetACLCommand is a SETACL command. +type SetACLCommand struct { + commandBase +} + +func (cmd *SetACLCommand) Wait() error { + return cmd.wait() +} + +// GetACL sends a GETACL command. +// +// This command requires support for the ACL extension. +func (c *Client) GetACL(mailbox string) *GetACLCommand { + cmd := &GetACLCommand{} + enc := c.beginCommand("GETACL", cmd) + enc.SP().Mailbox(mailbox) + enc.end() + return cmd +} + +// GetACLCommand is a GETACL command. +type GetACLCommand struct { + commandBase + data GetACLData +} + +func (cmd *GetACLCommand) Wait() (*GetACLData, error) { + return &cmd.data, cmd.wait() +} + +func (c *Client) handleMyRights() error { + data, err := readMyRights(c.dec) + if err != nil { + return fmt.Errorf("in myrights-response: %v", err) + } + if cmd := findPendingCmdByType[*MyRightsCommand](c); cmd != nil { + cmd.data = *data + } + return nil +} + +func (c *Client) handleGetACL() error { + data, err := readGetACL(c.dec) + if err != nil { + return fmt.Errorf("in getacl-response: %v", err) + } + if cmd := findPendingCmdByType[*GetACLCommand](c); cmd != nil { + cmd.data = *data + } + return nil +} + +// MyRightsCommand is a MYRIGHTS command. +type MyRightsCommand struct { + commandBase + data MyRightsData +} + +func (cmd *MyRightsCommand) Wait() (*MyRightsData, error) { + return &cmd.data, cmd.wait() +} + +// MyRightsData is the data returned by the MYRIGHTS command. +type MyRightsData struct { + Mailbox string + Rights imap.RightSet +} + +func readMyRights(dec *imapwire.Decoder) (*MyRightsData, error) { + var ( + rights string + data MyRightsData + ) + if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() || !dec.ExpectAString(&rights) { + return nil, dec.Err() + } + + data.Rights = imap.RightSet(rights) + return &data, nil +} + +// GetACLData is the data returned by the GETACL command. +type GetACLData struct { + Mailbox string + Rights map[imap.RightsIdentifier]imap.RightSet +} + +func readGetACL(dec *imapwire.Decoder) (*GetACLData, error) { + data := &GetACLData{Rights: make(map[imap.RightsIdentifier]imap.RightSet)} + + if !dec.ExpectMailbox(&data.Mailbox) { + return nil, dec.Err() + } + + for dec.SP() { + var rsStr, riStr string + if !dec.ExpectAString(&riStr) || !dec.ExpectSP() || !dec.ExpectAString(&rsStr) { + return nil, dec.Err() + } + + data.Rights[imap.RightsIdentifier(riStr)] = imap.RightSet(rsStr) + } + + return data, nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/append.go b/vendor/github.com/emersion/go-imap/v2/imapclient/append.go new file mode 100644 index 000000000..5bfff2357 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/append.go @@ -0,0 +1,58 @@ +package imapclient + +import ( + "io" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal" +) + +// Append sends an APPEND command. +// +// The caller must call AppendCommand.Close. +// +// The options are optional. +func (c *Client) Append(mailbox string, size int64, options *imap.AppendOptions) *AppendCommand { + cmd := &AppendCommand{} + cmd.enc = c.beginCommand("APPEND", cmd) + cmd.enc.SP().Mailbox(mailbox).SP() + if options != nil && len(options.Flags) > 0 { + cmd.enc.List(len(options.Flags), func(i int) { + cmd.enc.Flag(options.Flags[i]) + }).SP() + } + if options != nil && !options.Time.IsZero() { + cmd.enc.String(options.Time.Format(internal.DateTimeLayout)).SP() + } + // TODO: literal8 for BINARY + // TODO: UTF8 data ext for UTF8=ACCEPT, with literal8 + cmd.wc = cmd.enc.Literal(size) + return cmd +} + +// AppendCommand is an APPEND command. +// +// Callers must write the message contents, then call Close. +type AppendCommand struct { + commandBase + enc *commandEncoder + wc io.WriteCloser + data imap.AppendData +} + +func (cmd *AppendCommand) Write(b []byte) (int, error) { + return cmd.wc.Write(b) +} + +func (cmd *AppendCommand) Close() error { + err := cmd.wc.Close() + if cmd.enc != nil { + cmd.enc.end() + cmd.enc = nil + } + return err +} + +func (cmd *AppendCommand) Wait() (*imap.AppendData, error) { + return &cmd.data, cmd.wait() +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/authenticate.go b/vendor/github.com/emersion/go-imap/v2/imapclient/authenticate.go new file mode 100644 index 000000000..e0f67d0dc --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/authenticate.go @@ -0,0 +1,100 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-sasl" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal" +) + +// Authenticate sends an AUTHENTICATE command. +// +// Unlike other commands, this method blocks until the SASL exchange completes. +func (c *Client) Authenticate(saslClient sasl.Client) error { + mech, initialResp, err := saslClient.Start() + if err != nil { + return err + } + + // c.Caps may send a CAPABILITY command, so check it before c.beginCommand + var hasSASLIR bool + if initialResp != nil { + hasSASLIR = c.Caps().Has(imap.CapSASLIR) + } + + cmd := &authenticateCommand{} + contReq := c.registerContReq(cmd) + enc := c.beginCommand("AUTHENTICATE", cmd) + enc.SP().Atom(mech) + if initialResp != nil && hasSASLIR { + enc.SP().Atom(internal.EncodeSASL(initialResp)) + initialResp = nil + } + enc.flush() + defer enc.end() + + for { + challengeStr, err := contReq.Wait() + if err != nil { + return cmd.wait() + } + + if challengeStr == "" { + if initialResp == nil { + return fmt.Errorf("imapclient: server requested SASL initial response, but we don't have one") + } + + contReq = c.registerContReq(cmd) + if err := c.writeSASLResp(initialResp); err != nil { + return err + } + initialResp = nil + continue + } + + challenge, err := internal.DecodeSASL(challengeStr) + if err != nil { + return err + } + + resp, err := saslClient.Next(challenge) + if err != nil { + return err + } + + contReq = c.registerContReq(cmd) + if err := c.writeSASLResp(resp); err != nil { + return err + } + } +} + +type authenticateCommand struct { + commandBase +} + +func (c *Client) writeSASLResp(resp []byte) error { + respStr := internal.EncodeSASL(resp) + if _, err := c.bw.WriteString(respStr + "\r\n"); err != nil { + return err + } + if err := c.bw.Flush(); err != nil { + return err + } + return nil +} + +// Unauthenticate sends an UNAUTHENTICATE command. +// +// This command requires support for the UNAUTHENTICATE extension. +func (c *Client) Unauthenticate() *Command { + cmd := &unauthenticateCommand{} + c.beginCommand("UNAUTHENTICATE", cmd).end() + return &cmd.Command +} + +type unauthenticateCommand struct { + Command +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/capability.go b/vendor/github.com/emersion/go-imap/v2/imapclient/capability.go new file mode 100644 index 000000000..5e028f1b8 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/capability.go @@ -0,0 +1,55 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// Capability sends a CAPABILITY command. +func (c *Client) Capability() *CapabilityCommand { + cmd := &CapabilityCommand{} + c.beginCommand("CAPABILITY", cmd).end() + return cmd +} + +func (c *Client) handleCapability() error { + caps, err := readCapabilities(c.dec) + if err != nil { + return err + } + c.setCaps(caps) + if cmd := findPendingCmdByType[*CapabilityCommand](c); cmd != nil { + cmd.caps = caps + } + return nil +} + +// CapabilityCommand is a CAPABILITY command. +type CapabilityCommand struct { + commandBase + caps imap.CapSet +} + +func (cmd *CapabilityCommand) Wait() (imap.CapSet, error) { + err := cmd.wait() + return cmd.caps, err +} + +func readCapabilities(dec *imapwire.Decoder) (imap.CapSet, error) { + caps := make(imap.CapSet) + for dec.SP() { + // Some IMAP servers send multiple SP between caps: + // https://github.com/emersion/go-imap/pull/652 + for dec.SP() { + } + + var name string + if !dec.ExpectAtom(&name) { + return caps, fmt.Errorf("in capability-data: %v", dec.Err()) + } + caps[imap.Cap(name)] = struct{}{} + } + return caps, nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/client.go b/vendor/github.com/emersion/go-imap/v2/imapclient/client.go new file mode 100644 index 000000000..4bdd5c3ff --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/client.go @@ -0,0 +1,1215 @@ +// Package imapclient implements an IMAP client. +// +// # Charset decoding +// +// By default, only basic charset decoding is performed. For non-UTF-8 decoding +// of message subjects and e-mail address names, users can set +// Options.WordDecoder. For instance, to use go-message's collection of +// charsets: +// +// import ( +// "mime" +// +// "github.com/emersion/go-message/charset" +// ) +// +// options := &imapclient.Options{ +// WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader}, +// } +// client, err := imapclient.DialTLS("imap.example.org:993", options) +package imapclient + +import ( + "bufio" + "crypto/tls" + "errors" + "fmt" + "io" + "mime" + "net" + "runtime/debug" + "strconv" + "strings" + "sync" + "time" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +const ( + idleReadTimeout = time.Duration(0) + respReadTimeout = 30 * time.Second + literalReadTimeout = 5 * time.Minute + + cmdWriteTimeout = 30 * time.Second + literalWriteTimeout = 5 * time.Minute +) + +var dialer = &net.Dialer{ + Timeout: 30 * time.Second, +} + +// SelectedMailbox contains metadata for the currently selected mailbox. +type SelectedMailbox struct { + Name string + NumMessages uint32 + Flags []imap.Flag + PermanentFlags []imap.Flag +} + +func (mbox *SelectedMailbox) copy() *SelectedMailbox { + copy := *mbox + return © +} + +// Options contains options for Client. +type Options struct { + // TLS configuration for use by DialTLS and DialStartTLS. If nil, the + // default configuration is used. + TLSConfig *tls.Config + // Raw ingress and egress data will be written to this writer, if any. + // Note, this may include sensitive information such as credentials used + // during authentication. + DebugWriter io.Writer + // Unilateral data handler. + UnilateralDataHandler *UnilateralDataHandler + // Decoder for RFC 2047 words. + WordDecoder *mime.WordDecoder +} + +func (options *Options) wrapReadWriter(rw io.ReadWriter) io.ReadWriter { + if options.DebugWriter == nil { + return rw + } + return struct { + io.Reader + io.Writer + }{ + Reader: io.TeeReader(rw, options.DebugWriter), + Writer: io.MultiWriter(rw, options.DebugWriter), + } +} + +func (options *Options) decodeText(s string) (string, error) { + wordDecoder := options.WordDecoder + if wordDecoder == nil { + wordDecoder = &mime.WordDecoder{} + } + out, err := wordDecoder.DecodeHeader(s) + if err != nil { + return s, err + } + return out, nil +} + +func (options *Options) unilateralDataHandler() *UnilateralDataHandler { + if options.UnilateralDataHandler == nil { + return &UnilateralDataHandler{} + } + return options.UnilateralDataHandler +} + +func (options *Options) tlsConfig() *tls.Config { + if options != nil && options.TLSConfig != nil { + return options.TLSConfig.Clone() + } else { + return new(tls.Config) + } +} + +// Client is an IMAP client. +// +// IMAP commands are exposed as methods. These methods will block until the +// command has been sent to the server, but won't block until the server sends +// a response. They return a command struct which can be used to wait for the +// server response. This can be used to execute multiple commands concurrently, +// however care must be taken to avoid ambiguities. See RFC 9051 section 5.5. +// +// A client can be safely used from multiple goroutines, however this doesn't +// guarantee any command ordering and is subject to the same caveats as command +// pipelining (see above). Additionally, some commands (e.g. StartTLS, +// Authenticate, Idle) block the client during their execution. +type Client struct { + conn net.Conn + options Options + br *bufio.Reader + bw *bufio.Writer + dec *imapwire.Decoder + encMutex sync.Mutex + + greetingCh chan struct{} + greetingRecv bool + greetingErr error + + decCh chan struct{} + decErr error + + mutex sync.Mutex + state imap.ConnState + caps imap.CapSet + enabled imap.CapSet + pendingCapCh chan struct{} + mailbox *SelectedMailbox + cmdTag uint64 + pendingCmds []command + contReqs []continuationRequest + closed bool +} + +// New creates a new IMAP client. +// +// This function doesn't perform I/O. +// +// A nil options pointer is equivalent to a zero options value. +func New(conn net.Conn, options *Options) *Client { + if options == nil { + options = &Options{} + } + + rw := options.wrapReadWriter(conn) + br := bufio.NewReader(rw) + bw := bufio.NewWriter(rw) + + client := &Client{ + conn: conn, + options: *options, + br: br, + bw: bw, + dec: imapwire.NewDecoder(br, imapwire.ConnSideClient), + greetingCh: make(chan struct{}), + decCh: make(chan struct{}), + state: imap.ConnStateNone, + enabled: make(imap.CapSet), + } + go client.read() + return client +} + +// NewStartTLS creates a new IMAP client with STARTTLS. +// +// A nil options pointer is equivalent to a zero options value. +func NewStartTLS(conn net.Conn, options *Options) (*Client, error) { + if options == nil { + options = &Options{} + } + + client := New(conn, options) + if err := client.startTLS(options.TLSConfig); err != nil { + conn.Close() + return nil, err + } + + // Per section 7.1.4, refuse PREAUTH when using STARTTLS + if client.State() != imap.ConnStateNotAuthenticated { + client.Close() + return nil, fmt.Errorf("imapclient: server sent PREAUTH on unencrypted connection") + } + + return client, nil +} + +// DialInsecure connects to an IMAP server without any encryption at all. +func DialInsecure(address string, options *Options) (*Client, error) { + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, err + } + return New(conn, options), nil +} + +// DialTLS connects to an IMAP server with implicit TLS. +func DialTLS(address string, options *Options) (*Client, error) { + tlsConfig := options.tlsConfig() + if tlsConfig.NextProtos == nil { + tlsConfig.NextProtos = []string{"imap"} + } + + conn, err := tls.DialWithDialer(dialer, "tcp", address, tlsConfig) + if err != nil { + return nil, err + } + return New(conn, options), nil +} + +// DialStartTLS connects to an IMAP server with STARTTLS. +func DialStartTLS(address string, options *Options) (*Client, error) { + if options == nil { + options = &Options{} + } + + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + conn, err := dialer.Dial("tcp", address) + if err != nil { + return nil, err + } + + tlsConfig := options.tlsConfig() + if tlsConfig.ServerName == "" { + tlsConfig.ServerName = host + } + newOptions := *options + newOptions.TLSConfig = tlsConfig + return NewStartTLS(conn, &newOptions) +} + +func (c *Client) setReadTimeout(dur time.Duration) { + if dur > 0 { + c.conn.SetReadDeadline(time.Now().Add(dur)) + } else { + c.conn.SetReadDeadline(time.Time{}) + } +} + +func (c *Client) setWriteTimeout(dur time.Duration) { + if dur > 0 { + c.conn.SetWriteDeadline(time.Now().Add(dur)) + } else { + c.conn.SetWriteDeadline(time.Time{}) + } +} + +// State returns the current connection state of the client. +func (c *Client) State() imap.ConnState { + c.mutex.Lock() + defer c.mutex.Unlock() + return c.state +} + +func (c *Client) setState(state imap.ConnState) { + c.mutex.Lock() + c.state = state + if c.state != imap.ConnStateSelected { + c.mailbox = nil + } + c.mutex.Unlock() +} + +// Caps returns the capabilities advertised by the server. +// +// When the server hasn't sent the capability list, this method will request it +// and block until it's received. If the capabilities cannot be fetched, nil is +// returned. +func (c *Client) Caps() imap.CapSet { + if err := c.WaitGreeting(); err != nil { + return nil + } + + c.mutex.Lock() + caps := c.caps + capCh := c.pendingCapCh + c.mutex.Unlock() + + if caps != nil { + return caps + } + + if capCh == nil { + capCmd := c.Capability() + capCh := make(chan struct{}) + go func() { + capCmd.Wait() + close(capCh) + }() + c.mutex.Lock() + c.pendingCapCh = capCh + c.mutex.Unlock() + } + + timer := time.NewTimer(respReadTimeout) + defer timer.Stop() + select { + case <-timer.C: + return nil + case <-capCh: + // ok + } + + // TODO: this is racy if caps are reset before we get the reply + c.mutex.Lock() + defer c.mutex.Unlock() + return c.caps +} + +func (c *Client) setCaps(caps imap.CapSet) { + // If the capabilities are being reset, request the updated capabilities + // from the server + var capCh chan struct{} + if caps == nil { + capCh = make(chan struct{}) + + // We need to send the CAPABILITY command in a separate goroutine: + // setCaps might be called with Client.encMutex locked + go func() { + c.Capability().Wait() + close(capCh) + }() + } + + c.mutex.Lock() + c.caps = caps + c.pendingCapCh = capCh + c.mutex.Unlock() +} + +// Mailbox returns the state of the currently selected mailbox. +// +// If there is no currently selected mailbox, nil is returned. +// +// The returned struct must not be mutated. +func (c *Client) Mailbox() *SelectedMailbox { + c.mutex.Lock() + defer c.mutex.Unlock() + return c.mailbox +} + +// Close immediately closes the connection. +func (c *Client) Close() error { + c.mutex.Lock() + alreadyClosed := c.closed + c.closed = true + c.mutex.Unlock() + + // Ignore net.ErrClosed here, because we also call conn.Close in c.read + if err := c.conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.ErrClosedPipe) { + return err + } + + <-c.decCh + if err := c.decErr; err != nil { + return err + } + + if alreadyClosed { + return net.ErrClosed + } + return nil +} + +// beginCommand starts sending a command to the server. +// +// The command name and a space are written. +// +// The caller must call commandEncoder.end. +func (c *Client) beginCommand(name string, cmd command) *commandEncoder { + c.encMutex.Lock() // unlocked by commandEncoder.end + + c.mutex.Lock() + + c.cmdTag++ + tag := fmt.Sprintf("T%v", c.cmdTag) + + baseCmd := cmd.base() + *baseCmd = commandBase{ + tag: tag, + done: make(chan error, 1), + } + + c.pendingCmds = append(c.pendingCmds, cmd) + quotedUTF8 := c.caps.Has(imap.CapIMAP4rev2) || c.enabled.Has(imap.CapUTF8Accept) + literalMinus := c.caps.Has(imap.CapLiteralMinus) + literalPlus := c.caps.Has(imap.CapLiteralPlus) + + c.mutex.Unlock() + + c.setWriteTimeout(cmdWriteTimeout) + + wireEnc := imapwire.NewEncoder(c.bw, imapwire.ConnSideClient) + wireEnc.QuotedUTF8 = quotedUTF8 + wireEnc.LiteralMinus = literalMinus + wireEnc.LiteralPlus = literalPlus + wireEnc.NewContinuationRequest = func() *imapwire.ContinuationRequest { + return c.registerContReq(cmd) + } + + enc := &commandEncoder{ + Encoder: wireEnc, + client: c, + cmd: baseCmd, + } + enc.Atom(tag).SP().Atom(name) + return enc +} + +func (c *Client) deletePendingCmdByTag(tag string) command { + c.mutex.Lock() + defer c.mutex.Unlock() + + for i, cmd := range c.pendingCmds { + if cmd.base().tag == tag { + c.pendingCmds = append(c.pendingCmds[:i], c.pendingCmds[i+1:]...) + return cmd + } + } + return nil +} + +func (c *Client) findPendingCmdFunc(f func(cmd command) bool) command { + c.mutex.Lock() + defer c.mutex.Unlock() + + for _, cmd := range c.pendingCmds { + if f(cmd) { + return cmd + } + } + return nil +} + +func findPendingCmdByType[T command](c *Client) T { + c.mutex.Lock() + defer c.mutex.Unlock() + + for _, cmd := range c.pendingCmds { + if cmd, ok := cmd.(T); ok { + return cmd + } + } + + var cmd T + return cmd +} + +func (c *Client) completeCommand(cmd command, err error) { + done := cmd.base().done + done <- err + close(done) + + // Ensure the command is not blocked waiting on continuation requests + c.mutex.Lock() + var filtered []continuationRequest + for _, contReq := range c.contReqs { + if contReq.cmd != cmd.base() { + filtered = append(filtered, contReq) + } else { + contReq.Cancel(err) + } + } + c.contReqs = filtered + c.mutex.Unlock() + + switch cmd := cmd.(type) { + case *authenticateCommand, *loginCommand: + if err == nil { + c.setState(imap.ConnStateAuthenticated) + } + case *unauthenticateCommand: + if err == nil { + c.mutex.Lock() + c.state = imap.ConnStateNotAuthenticated + c.mailbox = nil + c.enabled = make(imap.CapSet) + c.mutex.Unlock() + } + case *SelectCommand: + if err == nil { + c.mutex.Lock() + c.state = imap.ConnStateSelected + c.mailbox = &SelectedMailbox{ + Name: cmd.mailbox, + NumMessages: cmd.data.NumMessages, + Flags: cmd.data.Flags, + PermanentFlags: cmd.data.PermanentFlags, + } + c.mutex.Unlock() + } + case *unselectCommand: + if err == nil { + c.setState(imap.ConnStateAuthenticated) + } + case *logoutCommand: + if err == nil { + c.setState(imap.ConnStateLogout) + } + case *ListCommand: + if cmd.pendingData != nil { + cmd.mailboxes <- cmd.pendingData + } + close(cmd.mailboxes) + case *FetchCommand: + close(cmd.msgs) + case *ExpungeCommand: + close(cmd.seqNums) + } +} + +func (c *Client) registerContReq(cmd command) *imapwire.ContinuationRequest { + contReq := imapwire.NewContinuationRequest() + + c.mutex.Lock() + c.contReqs = append(c.contReqs, continuationRequest{ + ContinuationRequest: contReq, + cmd: cmd.base(), + }) + c.mutex.Unlock() + + return contReq +} + +func (c *Client) closeWithError(err error) { + c.conn.Close() + + c.mutex.Lock() + c.state = imap.ConnStateLogout + pendingCmds := c.pendingCmds + c.pendingCmds = nil + c.mutex.Unlock() + + for _, cmd := range pendingCmds { + c.completeCommand(cmd, err) + } +} + +// read continuously reads data coming from the server. +// +// All the data is decoded in the read goroutine, then dispatched via channels +// to pending commands. +func (c *Client) read() { + defer close(c.decCh) + defer func() { + if v := recover(); v != nil { + c.decErr = fmt.Errorf("imapclient: panic reading response: %v\n%s", v, debug.Stack()) + } + + cmdErr := c.decErr + if cmdErr == nil { + cmdErr = io.ErrUnexpectedEOF + } + c.closeWithError(cmdErr) + }() + + c.setReadTimeout(respReadTimeout) // We're waiting for the greeting + for { + // Ignore net.ErrClosed here, because we also call conn.Close in c.Close + if c.dec.EOF() || errors.Is(c.dec.Err(), net.ErrClosed) || errors.Is(c.dec.Err(), io.ErrClosedPipe) { + break + } + if err := c.readResponse(); err != nil { + c.decErr = err + break + } + if c.greetingErr != nil { + break + } + } +} + +func (c *Client) readResponse() error { + c.setReadTimeout(respReadTimeout) + defer c.setReadTimeout(idleReadTimeout) + + if c.dec.Special('+') { + if err := c.readContinueReq(); err != nil { + return fmt.Errorf("in continue-req: %v", err) + } + return nil + } + + var tag, typ string + if !c.dec.Expect(c.dec.Special('*') || c.dec.Atom(&tag), "'*' or atom") { + return fmt.Errorf("in response: cannot read tag: %v", c.dec.Err()) + } + if !c.dec.ExpectSP() { + return fmt.Errorf("in response: %v", c.dec.Err()) + } + if !c.dec.ExpectAtom(&typ) { + return fmt.Errorf("in response: cannot read type: %v", c.dec.Err()) + } + + // Change typ to uppercase, as it's case-insensitive + typ = strings.ToUpper(typ) + + var ( + token string + err error + startTLS *startTLSCommand + ) + if tag != "" { + token = "response-tagged" + startTLS, err = c.readResponseTagged(tag, typ) + } else { + token = "response-data" + err = c.readResponseData(typ) + } + if err != nil { + return fmt.Errorf("in %v: %v", token, err) + } + + if !c.dec.ExpectCRLF() { + return fmt.Errorf("in response: %v", c.dec.Err()) + } + + if startTLS != nil { + c.upgradeStartTLS(startTLS) + } + + return nil +} + +func (c *Client) readContinueReq() error { + var text string + if c.dec.SP() { + c.dec.Text(&text) + } + if !c.dec.ExpectCRLF() { + return c.dec.Err() + } + + var contReq *imapwire.ContinuationRequest + c.mutex.Lock() + if len(c.contReqs) > 0 { + contReq = c.contReqs[0].ContinuationRequest + c.contReqs = append(c.contReqs[:0], c.contReqs[1:]...) + } + c.mutex.Unlock() + + if contReq == nil { + return fmt.Errorf("received unmatched continuation request") + } + + contReq.Done(text) + return nil +} + +func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand, err error) { + cmd := c.deletePendingCmdByTag(tag) + if cmd == nil { + return nil, fmt.Errorf("received tagged response with unknown tag %q", tag) + } + + // We've removed the command from the pending queue above. Make sure we + // don't stall it on error. + defer func() { + if err != nil { + c.completeCommand(cmd, err) + } + }() + + // Some servers don't provide a text even if the RFC requires it, + // see #500 and #502 + hasSP := c.dec.SP() + + var code string + if hasSP && c.dec.Special('[') { // resp-text-code + if !c.dec.ExpectAtom(&code) { + return nil, fmt.Errorf("in resp-text-code: %v", c.dec.Err()) + } + // TODO: LONGENTRIES and MAXSIZE from METADATA + switch code { + case "CAPABILITY": // capability-data + caps, err := readCapabilities(c.dec) + if err != nil { + return nil, fmt.Errorf("in capability-data: %v", err) + } + c.setCaps(caps) + case "APPENDUID": + var ( + uidValidity uint32 + uid imap.UID + ) + if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) || !c.dec.ExpectSP() || !c.dec.ExpectUID(&uid) { + return nil, fmt.Errorf("in resp-code-apnd: %v", c.dec.Err()) + } + if cmd, ok := cmd.(*AppendCommand); ok { + cmd.data.UID = uid + cmd.data.UIDValidity = uidValidity + } + case "COPYUID": + if !c.dec.ExpectSP() { + return nil, c.dec.Err() + } + uidValidity, srcUIDs, dstUIDs, err := readRespCodeCopyUID(c.dec) + if err != nil { + return nil, fmt.Errorf("in resp-code-copy: %v", err) + } + switch cmd := cmd.(type) { + case *CopyCommand: + cmd.data.UIDValidity = uidValidity + cmd.data.SourceUIDs = srcUIDs + cmd.data.DestUIDs = dstUIDs + case *MoveCommand: + // This can happen when Client.Move falls back to COPY + + // STORE + EXPUNGE + cmd.data.UIDValidity = uidValidity + cmd.data.SourceUIDs = srcUIDs + cmd.data.DestUIDs = dstUIDs + } + default: // [SP 1*] + if c.dec.SP() { + c.dec.DiscardUntilByte(']') + } + } + if !c.dec.ExpectSpecial(']') { + return nil, fmt.Errorf("in resp-text: %v", c.dec.Err()) + } + hasSP = c.dec.SP() + } + var text string + if hasSP && !c.dec.ExpectText(&text) { + return nil, fmt.Errorf("in resp-text: %v", c.dec.Err()) + } + + var cmdErr error + switch typ { + case "OK": + // nothing to do + case "NO", "BAD": + cmdErr = &imap.Error{ + Type: imap.StatusResponseType(typ), + Code: imap.ResponseCode(code), + Text: text, + } + default: + return nil, fmt.Errorf("in resp-cond-state: expected OK, NO or BAD status condition, but got %v", typ) + } + + c.completeCommand(cmd, cmdErr) + + if cmd, ok := cmd.(*startTLSCommand); ok && cmdErr == nil { + startTLS = cmd + } + + if cmdErr == nil && code != "CAPABILITY" { + switch cmd.(type) { + case *startTLSCommand, *loginCommand, *authenticateCommand, *unauthenticateCommand: + // These commands invalidate the capabilities + c.setCaps(nil) + } + } + + return startTLS, nil +} + +func (c *Client) readResponseData(typ string) error { + // number SP ("EXISTS" / "RECENT" / "FETCH" / "EXPUNGE") + var num uint32 + if typ[0] >= '0' && typ[0] <= '9' { + v, err := strconv.ParseUint(typ, 10, 32) + if err != nil { + return err + } + + num = uint32(v) + if !c.dec.ExpectSP() || !c.dec.ExpectAtom(&typ) { + return c.dec.Err() + } + } + + // All response type are case insensitive + switch strings.ToUpper(typ) { + case "OK", "PREAUTH", "NO", "BAD", "BYE": // resp-cond-state / resp-cond-bye / resp-cond-auth + // Some servers don't provide a text even if the RFC requires it, + // see #500 and #502 + hasSP := c.dec.SP() + + var code string + if hasSP && c.dec.Special('[') { // resp-text-code + if !c.dec.ExpectAtom(&code) { + return fmt.Errorf("in resp-text-code: %v", c.dec.Err()) + } + switch code { + case "CAPABILITY": // capability-data + caps, err := readCapabilities(c.dec) + if err != nil { + return fmt.Errorf("in capability-data: %v", err) + } + c.setCaps(caps) + case "PERMANENTFLAGS": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + flags, err := internal.ExpectFlagList(c.dec) + if err != nil { + return err + } + + c.mutex.Lock() + if c.state == imap.ConnStateSelected { + c.mailbox = c.mailbox.copy() + c.mailbox.PermanentFlags = flags + } + c.mutex.Unlock() + + if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil { + cmd.data.PermanentFlags = flags + } else if handler := c.options.unilateralDataHandler().Mailbox; handler != nil { + handler(&UnilateralDataMailbox{PermanentFlags: flags}) + } + case "UIDNEXT": + var uidNext imap.UID + if !c.dec.ExpectSP() || !c.dec.ExpectUID(&uidNext) { + return c.dec.Err() + } + if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil { + cmd.data.UIDNext = uidNext + } + case "UIDVALIDITY": + var uidValidity uint32 + if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) { + return c.dec.Err() + } + if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil { + cmd.data.UIDValidity = uidValidity + } + case "COPYUID": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + uidValidity, srcUIDs, dstUIDs, err := readRespCodeCopyUID(c.dec) + if err != nil { + return fmt.Errorf("in resp-code-copy: %v", err) + } + if cmd := findPendingCmdByType[*MoveCommand](c); cmd != nil { + cmd.data.UIDValidity = uidValidity + cmd.data.SourceUIDs = srcUIDs + cmd.data.DestUIDs = dstUIDs + } + case "HIGHESTMODSEQ": + var modSeq uint64 + if !c.dec.ExpectSP() || !c.dec.ExpectModSeq(&modSeq) { + return c.dec.Err() + } + if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil { + cmd.data.HighestModSeq = modSeq + } + case "NOMODSEQ": + // ignore + default: // [SP 1*] + if c.dec.SP() { + c.dec.DiscardUntilByte(']') + } + } + if !c.dec.ExpectSpecial(']') { + return fmt.Errorf("in resp-text: %v", c.dec.Err()) + } + hasSP = c.dec.SP() + } + + var text string + if hasSP && !c.dec.ExpectText(&text) { + return fmt.Errorf("in resp-text: %v", c.dec.Err()) + } + + if code == "CLOSED" { + c.setState(imap.ConnStateAuthenticated) + } + + if !c.greetingRecv { + switch typ { + case "OK": + c.setState(imap.ConnStateNotAuthenticated) + case "PREAUTH": + c.setState(imap.ConnStateAuthenticated) + default: + c.setState(imap.ConnStateLogout) + c.greetingErr = &imap.Error{ + Type: imap.StatusResponseType(typ), + Code: imap.ResponseCode(code), + Text: text, + } + } + c.greetingRecv = true + if c.greetingErr == nil && code != "CAPABILITY" { + c.setCaps(nil) // request initial capabilities + } + close(c.greetingCh) + } + case "ID": + return c.handleID() + case "CAPABILITY": + return c.handleCapability() + case "ENABLED": + return c.handleEnabled() + case "NAMESPACE": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleNamespace() + case "FLAGS": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleFlags() + case "EXISTS": + return c.handleExists(num) + case "RECENT": + // ignore + case "LIST": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleList() + case "STATUS": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleStatus() + case "FETCH": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleFetch(num) + case "EXPUNGE": + return c.handleExpunge(num) + case "SEARCH": + return c.handleSearch() + case "ESEARCH": + return c.handleESearch() + case "SORT": + return c.handleSort() + case "THREAD": + return c.handleThread() + case "METADATA": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleMetadata() + case "QUOTA": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleQuota() + case "QUOTAROOT": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleQuotaRoot() + case "MYRIGHTS": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleMyRights() + case "ACL": + if !c.dec.ExpectSP() { + return c.dec.Err() + } + return c.handleGetACL() + default: + return fmt.Errorf("unsupported response type %q", typ) + } + + return nil +} + +// WaitGreeting waits for the server's initial greeting. +func (c *Client) WaitGreeting() error { + select { + case <-c.greetingCh: + return c.greetingErr + case <-c.decCh: + if c.decErr != nil { + return fmt.Errorf("got error before greeting: %v", c.decErr) + } + return fmt.Errorf("connection closed before greeting") + } +} + +// Noop sends a NOOP command. +func (c *Client) Noop() *Command { + cmd := &Command{} + c.beginCommand("NOOP", cmd).end() + return cmd +} + +// Logout sends a LOGOUT command. +// +// This command informs the server that the client is done with the connection. +func (c *Client) Logout() *Command { + cmd := &logoutCommand{} + c.beginCommand("LOGOUT", cmd).end() + return &cmd.Command +} + +// Login sends a LOGIN command. +func (c *Client) Login(username, password string) *Command { + cmd := &loginCommand{} + enc := c.beginCommand("LOGIN", cmd) + enc.SP().String(username).SP().String(password) + enc.end() + return &cmd.Command +} + +// Delete sends a DELETE command. +func (c *Client) Delete(mailbox string) *Command { + cmd := &Command{} + enc := c.beginCommand("DELETE", cmd) + enc.SP().Mailbox(mailbox) + enc.end() + return cmd +} + +// Rename sends a RENAME command. +func (c *Client) Rename(mailbox, newName string) *Command { + cmd := &Command{} + enc := c.beginCommand("RENAME", cmd) + enc.SP().Mailbox(mailbox).SP().Mailbox(newName) + enc.end() + return cmd +} + +// Subscribe sends a SUBSCRIBE command. +func (c *Client) Subscribe(mailbox string) *Command { + cmd := &Command{} + enc := c.beginCommand("SUBSCRIBE", cmd) + enc.SP().Mailbox(mailbox) + enc.end() + return cmd +} + +// Subscribe sends an UNSUBSCRIBE command. +func (c *Client) Unsubscribe(mailbox string) *Command { + cmd := &Command{} + enc := c.beginCommand("UNSUBSCRIBE", cmd) + enc.SP().Mailbox(mailbox) + enc.end() + return cmd +} + +func uidCmdName(name string, kind imapwire.NumKind) string { + switch kind { + case imapwire.NumKindSeq: + return name + case imapwire.NumKindUID: + return "UID " + name + default: + panic("imapclient: invalid imapwire.NumKind") + } +} + +type commandEncoder struct { + *imapwire.Encoder + client *Client + cmd *commandBase +} + +// end ends an outgoing command. +// +// A CRLF is written, the encoder is flushed and its lock is released. +func (ce *commandEncoder) end() { + if ce.Encoder != nil { + ce.flush() + } + ce.client.setWriteTimeout(0) + ce.client.encMutex.Unlock() +} + +// flush sends an outgoing command, but keeps the encoder lock. +// +// A CRLF is written and the encoder is flushed. Callers must call +// commandEncoder.end to release the lock. +func (ce *commandEncoder) flush() { + if err := ce.Encoder.CRLF(); err != nil { + // TODO: consider stashing the error in Client to return it in future + // calls + ce.client.closeWithError(err) + } + ce.Encoder = nil +} + +// Literal encodes a literal. +func (ce *commandEncoder) Literal(size int64) io.WriteCloser { + var contReq *imapwire.ContinuationRequest + ce.client.mutex.Lock() + hasCapLiteralMinus := ce.client.caps.Has(imap.CapLiteralMinus) + ce.client.mutex.Unlock() + if size > 4096 || !hasCapLiteralMinus { + contReq = ce.client.registerContReq(ce.cmd) + } + ce.client.setWriteTimeout(literalWriteTimeout) + return literalWriter{ + WriteCloser: ce.Encoder.Literal(size, contReq), + client: ce.client, + } +} + +type literalWriter struct { + io.WriteCloser + client *Client +} + +func (lw literalWriter) Close() error { + lw.client.setWriteTimeout(cmdWriteTimeout) + return lw.WriteCloser.Close() +} + +// continuationRequest is a pending continuation request. +type continuationRequest struct { + *imapwire.ContinuationRequest + cmd *commandBase +} + +// UnilateralDataMailbox describes a mailbox status update. +// +// If a field is nil, it hasn't changed. +type UnilateralDataMailbox struct { + NumMessages *uint32 + Flags []imap.Flag + PermanentFlags []imap.Flag +} + +// UnilateralDataHandler handles unilateral data. +// +// The handler will block the client while running. If the caller intends to +// perform slow operations, a buffered channel and a separate goroutine should +// be used. +// +// The handler will be invoked in an arbitrary goroutine. +// +// See Options.UnilateralDataHandler. +type UnilateralDataHandler struct { + Expunge func(seqNum uint32) + Mailbox func(data *UnilateralDataMailbox) + Fetch func(msg *FetchMessageData) + + // requires ENABLE METADATA or ENABLE SERVER-METADATA + Metadata func(mailbox string, entries []string) +} + +// command is an interface for IMAP commands. +// +// Commands are represented by the Command type, but can be extended by other +// types (e.g. CapabilityCommand). +type command interface { + base() *commandBase +} + +type commandBase struct { + tag string + done chan error + err error +} + +func (cmd *commandBase) base() *commandBase { + return cmd +} + +func (cmd *commandBase) wait() error { + if cmd.err == nil { + cmd.err = <-cmd.done + } + return cmd.err +} + +// Command is a basic IMAP command. +type Command struct { + commandBase +} + +// Wait blocks until the command has completed. +func (cmd *Command) Wait() error { + return cmd.wait() +} + +type loginCommand struct { + Command +} + +// logoutCommand is a LOGOUT command. +type logoutCommand struct { + Command +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/copy.go b/vendor/github.com/emersion/go-imap/v2/imapclient/copy.go new file mode 100644 index 000000000..c1081d825 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/copy.go @@ -0,0 +1,37 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// Copy sends a COPY command. +func (c *Client) Copy(numSet imap.NumSet, mailbox string) *CopyCommand { + cmd := &CopyCommand{} + enc := c.beginCommand(uidCmdName("COPY", imapwire.NumSetKind(numSet)), cmd) + enc.SP().NumSet(numSet).SP().Mailbox(mailbox) + enc.end() + return cmd +} + +// CopyCommand is a COPY command. +type CopyCommand struct { + commandBase + data imap.CopyData +} + +func (cmd *CopyCommand) Wait() (*imap.CopyData, error) { + return &cmd.data, cmd.wait() +} + +func readRespCodeCopyUID(dec *imapwire.Decoder) (uidValidity uint32, srcUIDs, dstUIDs imap.UIDSet, err error) { + if !dec.ExpectNumber(&uidValidity) || !dec.ExpectSP() || !dec.ExpectUIDSet(&srcUIDs) || !dec.ExpectSP() || !dec.ExpectUIDSet(&dstUIDs) { + return 0, nil, nil, dec.Err() + } + if srcUIDs.Dynamic() || dstUIDs.Dynamic() { + return 0, nil, nil, fmt.Errorf("imapclient: server returned dynamic number set in COPYUID response") + } + return uidValidity, srcUIDs, dstUIDs, nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/create.go b/vendor/github.com/emersion/go-imap/v2/imapclient/create.go new file mode 100644 index 000000000..827ecce99 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/create.go @@ -0,0 +1,21 @@ +package imapclient + +import ( + "github.com/emersion/go-imap/v2" +) + +// Create sends a CREATE command. +// +// A nil options pointer is equivalent to a zero options value. +func (c *Client) Create(mailbox string, options *imap.CreateOptions) *Command { + cmd := &Command{} + enc := c.beginCommand("CREATE", cmd) + enc.SP().Mailbox(mailbox) + if options != nil && len(options.SpecialUse) > 0 { + enc.SP().Special('(').Atom("USE").SP().List(len(options.SpecialUse), func(i int) { + enc.MailboxAttr(options.SpecialUse[i]) + }).Special(')') + } + enc.end() + return cmd +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/enable.go b/vendor/github.com/emersion/go-imap/v2/imapclient/enable.go new file mode 100644 index 000000000..895766643 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/enable.go @@ -0,0 +1,69 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" +) + +// Enable sends an ENABLE command. +// +// This command requires support for IMAP4rev2 or the ENABLE extension. +func (c *Client) Enable(caps ...imap.Cap) *EnableCommand { + // Enabling an extension may change the IMAP syntax, so only allow the + // extensions we support here + for _, name := range caps { + switch name { + case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer: + // ok + default: + done := make(chan error) + close(done) + err := fmt.Errorf("imapclient: cannot enable %q: not supported", name) + return &EnableCommand{commandBase: commandBase{done: done, err: err}} + } + } + + cmd := &EnableCommand{} + enc := c.beginCommand("ENABLE", cmd) + for _, c := range caps { + enc.SP().Atom(string(c)) + } + enc.end() + return cmd +} + +func (c *Client) handleEnabled() error { + caps, err := readCapabilities(c.dec) + if err != nil { + return err + } + + c.mutex.Lock() + for name := range caps { + c.enabled[name] = struct{}{} + } + c.mutex.Unlock() + + if cmd := findPendingCmdByType[*EnableCommand](c); cmd != nil { + cmd.data.Caps = caps + } + + return nil +} + +// EnableCommand is an ENABLE command. +type EnableCommand struct { + commandBase + data EnableData +} + +func (cmd *EnableCommand) Wait() (*EnableData, error) { + return &cmd.data, cmd.wait() +} + +// EnableData is the data returned by the ENABLE command. +type EnableData struct { + // Capabilities that were successfully enabled + Caps imap.CapSet +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/expunge.go b/vendor/github.com/emersion/go-imap/v2/imapclient/expunge.go new file mode 100644 index 000000000..11e477c1f --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/expunge.go @@ -0,0 +1,84 @@ +package imapclient + +import ( + "github.com/emersion/go-imap/v2" +) + +// Expunge sends an EXPUNGE command. +func (c *Client) Expunge() *ExpungeCommand { + cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)} + c.beginCommand("EXPUNGE", cmd).end() + return cmd +} + +// UIDExpunge sends a UID EXPUNGE command. +// +// This command requires support for IMAP4rev2 or the UIDPLUS extension. +func (c *Client) UIDExpunge(uids imap.UIDSet) *ExpungeCommand { + cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)} + enc := c.beginCommand("UID EXPUNGE", cmd) + enc.SP().NumSet(uids) + enc.end() + return cmd +} + +func (c *Client) handleExpunge(seqNum uint32) error { + c.mutex.Lock() + if c.state == imap.ConnStateSelected && c.mailbox.NumMessages > 0 { + c.mailbox = c.mailbox.copy() + c.mailbox.NumMessages-- + } + c.mutex.Unlock() + + cmd := findPendingCmdByType[*ExpungeCommand](c) + if cmd != nil { + cmd.seqNums <- seqNum + } else if handler := c.options.unilateralDataHandler().Expunge; handler != nil { + handler(seqNum) + } + + return nil +} + +// ExpungeCommand is an EXPUNGE command. +// +// The caller must fully consume the ExpungeCommand. A simple way to do so is +// to defer a call to FetchCommand.Close. +type ExpungeCommand struct { + commandBase + seqNums chan uint32 +} + +// Next advances to the next expunged message sequence number. +// +// On success, the message sequence number is returned. On error or if there +// are no more messages, 0 is returned. To check the error value, use Close. +func (cmd *ExpungeCommand) Next() uint32 { + return <-cmd.seqNums +} + +// Close releases the command. +// +// Calling Close unblocks the IMAP client decoder and lets it read the next +// responses. Next will always return nil after Close. +func (cmd *ExpungeCommand) Close() error { + for cmd.Next() != 0 { + // ignore + } + return cmd.wait() +} + +// Collect accumulates expunged sequence numbers into a list. +// +// This is equivalent to calling Next repeatedly and then Close. +func (cmd *ExpungeCommand) Collect() ([]uint32, error) { + var l []uint32 + for { + seqNum := cmd.Next() + if seqNum == 0 { + break + } + l = append(l, seqNum) + } + return l, cmd.Close() +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/fetch.go b/vendor/github.com/emersion/go-imap/v2/imapclient/fetch.go new file mode 100644 index 000000000..74d95f133 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/fetch.go @@ -0,0 +1,1326 @@ +package imapclient + +import ( + "fmt" + "io" + netmail "net/mail" + "strings" + "time" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal" + "github.com/emersion/go-imap/v2/internal/imapwire" + "github.com/emersion/go-message/mail" +) + +// Fetch sends a FETCH command. +// +// The caller must fully consume the FetchCommand. A simple way to do so is to +// defer a call to FetchCommand.Close. +// +// A nil options pointer is equivalent to a zero options value. +func (c *Client) Fetch(numSet imap.NumSet, options *imap.FetchOptions) *FetchCommand { + if options == nil { + options = new(imap.FetchOptions) + } + + numKind := imapwire.NumSetKind(numSet) + + cmd := &FetchCommand{ + numSet: numSet, + msgs: make(chan *FetchMessageData, 128), + } + enc := c.beginCommand(uidCmdName("FETCH", numKind), cmd) + enc.SP().NumSet(numSet).SP() + writeFetchItems(enc.Encoder, numKind, options) + if options.ChangedSince != 0 { + enc.SP().Special('(').Atom("CHANGEDSINCE").SP().ModSeq(options.ChangedSince).Special(')') + } + enc.end() + return cmd +} + +func writeFetchItems(enc *imapwire.Encoder, numKind imapwire.NumKind, options *imap.FetchOptions) { + listEnc := enc.BeginList() + + // Ensure we request UID as the first data item for UID FETCH, to be safer. + // We want to get it before any literal. + if options.UID || numKind == imapwire.NumKindUID { + listEnc.Item().Atom("UID") + } + + m := map[string]bool{ + "BODY": options.BodyStructure != nil && !options.BodyStructure.Extended, + "BODYSTRUCTURE": options.BodyStructure != nil && options.BodyStructure.Extended, + "ENVELOPE": options.Envelope, + "FLAGS": options.Flags, + "INTERNALDATE": options.InternalDate, + "RFC822.SIZE": options.RFC822Size, + "MODSEQ": options.ModSeq, + } + for k, req := range m { + if req { + listEnc.Item().Atom(k) + } + } + + for _, bs := range options.BodySection { + writeFetchItemBodySection(listEnc.Item(), bs) + } + for _, bs := range options.BinarySection { + writeFetchItemBinarySection(listEnc.Item(), bs) + } + for _, bss := range options.BinarySectionSize { + writeFetchItemBinarySectionSize(listEnc.Item(), bss) + } + + listEnc.End() +} + +func writeFetchItemBodySection(enc *imapwire.Encoder, item *imap.FetchItemBodySection) { + enc.Atom("BODY") + if item.Peek { + enc.Atom(".PEEK") + } + enc.Special('[') + writeSectionPart(enc, item.Part) + if len(item.Part) > 0 && item.Specifier != imap.PartSpecifierNone { + enc.Special('.') + } + if item.Specifier != imap.PartSpecifierNone { + enc.Atom(string(item.Specifier)) + + var headerList []string + if len(item.HeaderFields) > 0 { + headerList = item.HeaderFields + enc.Atom(".FIELDS") + } else if len(item.HeaderFieldsNot) > 0 { + headerList = item.HeaderFieldsNot + enc.Atom(".FIELDS.NOT") + } + + if len(headerList) > 0 { + enc.SP().List(len(headerList), func(i int) { + enc.String(headerList[i]) + }) + } + } + enc.Special(']') + writeSectionPartial(enc, item.Partial) +} + +func writeFetchItemBinarySection(enc *imapwire.Encoder, item *imap.FetchItemBinarySection) { + enc.Atom("BINARY") + if item.Peek { + enc.Atom(".PEEK") + } + enc.Special('[') + writeSectionPart(enc, item.Part) + enc.Special(']') + writeSectionPartial(enc, item.Partial) +} + +func writeFetchItemBinarySectionSize(enc *imapwire.Encoder, item *imap.FetchItemBinarySectionSize) { + enc.Atom("BINARY.SIZE") + enc.Special('[') + writeSectionPart(enc, item.Part) + enc.Special(']') +} + +func writeSectionPart(enc *imapwire.Encoder, part []int) { + if len(part) == 0 { + return + } + + var l []string + for _, num := range part { + l = append(l, fmt.Sprintf("%v", num)) + } + enc.Atom(strings.Join(l, ".")) +} + +func writeSectionPartial(enc *imapwire.Encoder, partial *imap.SectionPartial) { + if partial == nil { + return + } + enc.Special('<').Number64(partial.Offset).Special('.').Number64(partial.Size).Special('>') +} + +// FetchCommand is a FETCH command. +type FetchCommand struct { + commandBase + + numSet imap.NumSet + recvSeqSet imap.SeqSet + recvUIDSet imap.UIDSet + + msgs chan *FetchMessageData + prev *FetchMessageData +} + +func (cmd *FetchCommand) recvSeqNum(seqNum uint32) bool { + set, ok := cmd.numSet.(imap.SeqSet) + if !ok || !set.Contains(seqNum) { + return false + } + + if cmd.recvSeqSet.Contains(seqNum) { + return false + } + + cmd.recvSeqSet.AddNum(seqNum) + return true +} + +func (cmd *FetchCommand) recvUID(uid imap.UID) bool { + set, ok := cmd.numSet.(imap.UIDSet) + if !ok || !set.Contains(uid) { + return false + } + + if cmd.recvUIDSet.Contains(uid) { + return false + } + + cmd.recvUIDSet.AddNum(uid) + return true +} + +// Next advances to the next message. +// +// On success, the message is returned. On error or if there are no more +// messages, nil is returned. To check the error value, use Close. +func (cmd *FetchCommand) Next() *FetchMessageData { + if cmd.prev != nil { + cmd.prev.discard() + } + cmd.prev = <-cmd.msgs + return cmd.prev +} + +// Close releases the command. +// +// Calling Close unblocks the IMAP client decoder and lets it read the next +// responses. Next will always return nil after Close. +func (cmd *FetchCommand) Close() error { + for cmd.Next() != nil { + // ignore + } + return cmd.wait() +} + +// Collect accumulates message data into a list. +// +// This method will read and store message contents in memory. This is +// acceptable when the message contents have a reasonable size, but may not be +// suitable when fetching e.g. attachments. +// +// This is equivalent to calling Next repeatedly and then Close. +func (cmd *FetchCommand) Collect() ([]*FetchMessageBuffer, error) { + defer cmd.Close() + + var l []*FetchMessageBuffer + for { + msg := cmd.Next() + if msg == nil { + break + } + + buf, err := msg.Collect() + if err != nil { + return l, err + } + + l = append(l, buf) + } + return l, cmd.Close() +} + +func matchFetchItemBodySection(cmd, resp *imap.FetchItemBodySection) bool { + if cmd.Specifier != resp.Specifier { + return false + } + + if !intSliceEqual(cmd.Part, resp.Part) { + return false + } + if !stringSliceEqualFold(cmd.HeaderFields, resp.HeaderFields) { + return false + } + if !stringSliceEqualFold(cmd.HeaderFieldsNot, resp.HeaderFieldsNot) { + return false + } + + if (cmd.Partial == nil) != (resp.Partial == nil) { + return false + } + if cmd.Partial != nil && cmd.Partial.Offset != resp.Partial.Offset { + return false + } + + // Ignore Partial.Size and Peek: these are not echoed back by the server + return true +} + +func matchFetchItemBinarySection(cmd, resp *imap.FetchItemBinarySection) bool { + // Ignore Partial and Peek: these are not echoed back by the server + return intSliceEqual(cmd.Part, resp.Part) +} + +func intSliceEqual(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func stringSliceEqualFold(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !strings.EqualFold(a[i], b[i]) { + return false + } + } + return true +} + +// FetchMessageData contains a message's FETCH data. +type FetchMessageData struct { + SeqNum uint32 + + items chan FetchItemData + prev FetchItemData +} + +// Next advances to the next data item for this message. +// +// If there is one or more data items left, the next item is returned. +// Otherwise nil is returned. +func (data *FetchMessageData) Next() FetchItemData { + if d, ok := data.prev.(discarder); ok { + d.discard() + } + + item := <-data.items + data.prev = item + return item +} + +func (data *FetchMessageData) discard() { + for { + if item := data.Next(); item == nil { + break + } + } +} + +// Collect accumulates message data into a struct. +// +// This method will read and store message contents in memory. This is +// acceptable when the message contents have a reasonable size, but may not be +// suitable when fetching e.g. attachments. +func (data *FetchMessageData) Collect() (*FetchMessageBuffer, error) { + defer data.discard() + + buf := &FetchMessageBuffer{SeqNum: data.SeqNum} + for { + item := data.Next() + if item == nil { + break + } + if err := buf.populateItemData(item); err != nil { + return buf, err + } + } + return buf, nil +} + +// FetchItemData contains a message's FETCH item data. +type FetchItemData interface { + fetchItemData() +} + +var ( + _ FetchItemData = FetchItemDataBodySection{} + _ FetchItemData = FetchItemDataBinarySection{} + _ FetchItemData = FetchItemDataFlags{} + _ FetchItemData = FetchItemDataEnvelope{} + _ FetchItemData = FetchItemDataInternalDate{} + _ FetchItemData = FetchItemDataRFC822Size{} + _ FetchItemData = FetchItemDataUID{} + _ FetchItemData = FetchItemDataBodyStructure{} +) + +type discarder interface { + discard() +} + +var ( + _ discarder = FetchItemDataBodySection{} + _ discarder = FetchItemDataBinarySection{} +) + +// FetchItemDataBodySection holds data returned by FETCH BODY[]. +// +// Literal might be nil. +type FetchItemDataBodySection struct { + Section *imap.FetchItemBodySection + Literal imap.LiteralReader +} + +func (FetchItemDataBodySection) fetchItemData() {} + +func (item FetchItemDataBodySection) discard() { + if item.Literal != nil { + io.Copy(io.Discard, item.Literal) + } +} + +// MatchCommand checks whether a section returned by the server in a response +// is compatible with a section requested by the client in a command. +func (dataItem *FetchItemDataBodySection) MatchCommand(item *imap.FetchItemBodySection) bool { + return matchFetchItemBodySection(item, dataItem.Section) +} + +// FetchItemDataBinarySection holds data returned by FETCH BINARY[]. +// +// Literal might be nil. +type FetchItemDataBinarySection struct { + Section *imap.FetchItemBinarySection + Literal imap.LiteralReader +} + +func (FetchItemDataBinarySection) fetchItemData() {} + +func (item FetchItemDataBinarySection) discard() { + if item.Literal != nil { + io.Copy(io.Discard, item.Literal) + } +} + +// MatchCommand checks whether a section returned by the server in a response +// is compatible with a section requested by the client in a command. +func (dataItem *FetchItemDataBinarySection) MatchCommand(item *imap.FetchItemBinarySection) bool { + return matchFetchItemBinarySection(item, dataItem.Section) +} + +// FetchItemDataFlags holds data returned by FETCH FLAGS. +type FetchItemDataFlags struct { + Flags []imap.Flag +} + +func (FetchItemDataFlags) fetchItemData() {} + +// FetchItemDataEnvelope holds data returned by FETCH ENVELOPE. +type FetchItemDataEnvelope struct { + Envelope *imap.Envelope +} + +func (FetchItemDataEnvelope) fetchItemData() {} + +// FetchItemDataInternalDate holds data returned by FETCH INTERNALDATE. +type FetchItemDataInternalDate struct { + Time time.Time +} + +func (FetchItemDataInternalDate) fetchItemData() {} + +// FetchItemDataRFC822Size holds data returned by FETCH RFC822.SIZE. +type FetchItemDataRFC822Size struct { + Size int64 +} + +func (FetchItemDataRFC822Size) fetchItemData() {} + +// FetchItemDataUID holds data returned by FETCH UID. +type FetchItemDataUID struct { + UID imap.UID +} + +func (FetchItemDataUID) fetchItemData() {} + +// FetchItemDataBodyStructure holds data returned by FETCH BODYSTRUCTURE or +// FETCH BODY. +type FetchItemDataBodyStructure struct { + BodyStructure imap.BodyStructure + IsExtended bool // True if BODYSTRUCTURE, false if BODY +} + +func (FetchItemDataBodyStructure) fetchItemData() {} + +// FetchItemDataBinarySectionSize holds data returned by FETCH BINARY.SIZE[]. +type FetchItemDataBinarySectionSize struct { + Part []int + Size uint32 +} + +func (FetchItemDataBinarySectionSize) fetchItemData() {} + +// MatchCommand checks whether a section size returned by the server in a +// response is compatible with a section size requested by the client in a +// command. +func (data *FetchItemDataBinarySectionSize) MatchCommand(item *imap.FetchItemBinarySectionSize) bool { + return intSliceEqual(item.Part, data.Part) +} + +// FetchItemDataModSeq holds data returned by FETCH MODSEQ. +// +// This requires the CONDSTORE extension. +type FetchItemDataModSeq struct { + ModSeq uint64 +} + +func (FetchItemDataModSeq) fetchItemData() {} + +// FetchBodySectionBuffer is a buffer for the data returned by +// FetchItemBodySection. +type FetchBodySectionBuffer struct { + Section *imap.FetchItemBodySection + Bytes []byte +} + +// FetchBinarySectionBuffer is a buffer for the data returned by +// FetchItemBinarySection. +type FetchBinarySectionBuffer struct { + Section *imap.FetchItemBinarySection + Bytes []byte +} + +// FetchMessageBuffer is a buffer for the data returned by FetchMessageData. +// +// The SeqNum field is always populated. All remaining fields are optional. +type FetchMessageBuffer struct { + SeqNum uint32 + Flags []imap.Flag + Envelope *imap.Envelope + InternalDate time.Time + RFC822Size int64 + UID imap.UID + BodyStructure imap.BodyStructure + BodySection []FetchBodySectionBuffer + BinarySection []FetchBinarySectionBuffer + BinarySectionSize []FetchItemDataBinarySectionSize + ModSeq uint64 // requires CONDSTORE +} + +func (buf *FetchMessageBuffer) populateItemData(item FetchItemData) error { + switch item := item.(type) { + case FetchItemDataBodySection: + var b []byte + if item.Literal != nil { + var err error + b, err = io.ReadAll(item.Literal) + if err != nil { + return err + } + } + buf.BodySection = append(buf.BodySection, FetchBodySectionBuffer{ + Section: item.Section, + Bytes: b, + }) + case FetchItemDataBinarySection: + var b []byte + if item.Literal != nil { + var err error + b, err = io.ReadAll(item.Literal) + if err != nil { + return err + } + } + buf.BinarySection = append(buf.BinarySection, FetchBinarySectionBuffer{ + Section: item.Section, + Bytes: b, + }) + case FetchItemDataFlags: + buf.Flags = item.Flags + case FetchItemDataEnvelope: + buf.Envelope = item.Envelope + case FetchItemDataInternalDate: + buf.InternalDate = item.Time + case FetchItemDataRFC822Size: + buf.RFC822Size = item.Size + case FetchItemDataUID: + buf.UID = item.UID + case FetchItemDataBodyStructure: + buf.BodyStructure = item.BodyStructure + case FetchItemDataBinarySectionSize: + buf.BinarySectionSize = append(buf.BinarySectionSize, item) + case FetchItemDataModSeq: + buf.ModSeq = item.ModSeq + default: + panic(fmt.Errorf("unsupported fetch item data %T", item)) + } + return nil +} + +// FindBodySection returns the contents of a requested body section. +// +// If the body section is not found, nil is returned. +func (buf *FetchMessageBuffer) FindBodySection(section *imap.FetchItemBodySection) []byte { + for _, s := range buf.BodySection { + if matchFetchItemBodySection(section, s.Section) { + return s.Bytes + } + } + return nil +} + +// FindBinarySection returns the contents of a requested binary section. +// +// If the binary section is not found, nil is returned. +func (buf *FetchMessageBuffer) FindBinarySection(section *imap.FetchItemBinarySection) []byte { + for _, s := range buf.BinarySection { + if matchFetchItemBinarySection(section, s.Section) { + return s.Bytes + } + } + return nil +} + +// FindBinarySectionSize returns a requested binary section size. +// +// If the binary section size is not found, false is returned. +func (buf *FetchMessageBuffer) FindBinarySectionSize(part []int) (uint32, bool) { + for _, s := range buf.BinarySectionSize { + if intSliceEqual(part, s.Part) { + return s.Size, true + } + } + return 0, false +} + +func (c *Client) handleFetch(seqNum uint32) error { + dec := c.dec + + items := make(chan FetchItemData, 32) + defer close(items) + + msg := &FetchMessageData{SeqNum: seqNum, items: items} + + // We're in a tricky situation: to know whether this FETCH response needs + // to be handled by a pending command, we may need to look at the UID in + // the response data. But the response data comes in in a streaming + // fashion: it can contain literals. Assume that the UID will be returned + // before any literal. + var uid imap.UID + handled := false + handleMsg := func() { + if handled { + return + } + + cmd := c.findPendingCmdFunc(func(anyCmd command) bool { + cmd, ok := anyCmd.(*FetchCommand) + if !ok { + return false + } + + // Skip if we haven't requested or already handled this message + if _, ok := cmd.numSet.(imap.UIDSet); ok { + return uid != 0 && cmd.recvUID(uid) + } else { + return seqNum != 0 && cmd.recvSeqNum(seqNum) + } + }) + if cmd != nil { + cmd := cmd.(*FetchCommand) + cmd.msgs <- msg + } else if handler := c.options.unilateralDataHandler().Fetch; handler != nil { + go handler(msg) + } else { + go msg.discard() + } + + handled = true + } + defer handleMsg() + + numAtts := 0 + return dec.ExpectList(func() error { + var attName string + if !dec.Expect(dec.Func(&attName, isMsgAttNameChar), "msg-att name") { + return dec.Err() + } + attName = strings.ToUpper(attName) + + var ( + item FetchItemData + done chan struct{} + ) + switch attName { + case "FLAGS": + if !dec.ExpectSP() { + return dec.Err() + } + + flags, err := internal.ExpectFlagList(dec) + if err != nil { + return err + } + + item = FetchItemDataFlags{Flags: flags} + case "ENVELOPE": + if !dec.ExpectSP() { + return dec.Err() + } + + envelope, err := readEnvelope(dec, &c.options) + if err != nil { + return fmt.Errorf("in envelope: %v", err) + } + + item = FetchItemDataEnvelope{Envelope: envelope} + case "INTERNALDATE": + if !dec.ExpectSP() { + return dec.Err() + } + + t, err := internal.ExpectDateTime(dec) + if err != nil { + return err + } + + item = FetchItemDataInternalDate{Time: t} + case "RFC822.SIZE": + var size int64 + if !dec.ExpectSP() || !dec.ExpectNumber64(&size) { + return dec.Err() + } + + item = FetchItemDataRFC822Size{Size: size} + case "UID": + if !dec.ExpectSP() || !dec.ExpectUID(&uid) { + return dec.Err() + } + + item = FetchItemDataUID{UID: uid} + case "BODY", "BINARY": + if dec.Special('[') { + var section interface{} + switch attName { + case "BODY": + var err error + section, err = readSectionSpec(dec) + if err != nil { + return fmt.Errorf("in section-spec: %v", err) + } + case "BINARY": + part, dot := readSectionPart(dec) + if dot { + return fmt.Errorf("in section-binary: expected number after dot") + } + if !dec.ExpectSpecial(']') { + return dec.Err() + } + section = &imap.FetchItemBinarySection{Part: part} + } + + if !dec.ExpectSP() { + return dec.Err() + } + + // Ignore literal8 marker, if any + if attName == "BINARY" { + dec.Special('~') + } + + lit, _, ok := dec.ExpectNStringReader() + if !ok { + return dec.Err() + } + + var fetchLit imap.LiteralReader + if lit != nil { + done = make(chan struct{}) + fetchLit = &fetchLiteralReader{ + LiteralReader: lit, + ch: done, + } + } + + switch section := section.(type) { + case *imap.FetchItemBodySection: + item = FetchItemDataBodySection{ + Section: section, + Literal: fetchLit, + } + case *imap.FetchItemBinarySection: + item = FetchItemDataBinarySection{ + Section: section, + Literal: fetchLit, + } + } + break + } + if !dec.Expect(attName == "BODY", "'['") { + return dec.Err() + } + fallthrough + case "BODYSTRUCTURE": + if !dec.ExpectSP() { + return dec.Err() + } + + bodyStruct, err := readBody(dec, &c.options) + if err != nil { + return err + } + + item = FetchItemDataBodyStructure{ + BodyStructure: bodyStruct, + IsExtended: attName == "BODYSTRUCTURE", + } + case "BINARY.SIZE": + if !dec.ExpectSpecial('[') { + return dec.Err() + } + part, dot := readSectionPart(dec) + if dot { + return fmt.Errorf("in section-binary: expected number after dot") + } + + var size uint32 + if !dec.ExpectSpecial(']') || !dec.ExpectSP() || !dec.ExpectNumber(&size) { + return dec.Err() + } + + item = FetchItemDataBinarySectionSize{ + Part: part, + Size: size, + } + case "MODSEQ": + var modSeq uint64 + if !dec.ExpectSP() || !dec.ExpectSpecial('(') || !dec.ExpectModSeq(&modSeq) || !dec.ExpectSpecial(')') { + return dec.Err() + } + item = FetchItemDataModSeq{ModSeq: modSeq} + default: + return fmt.Errorf("unsupported msg-att name: %q", attName) + } + + numAtts++ + if numAtts > cap(items) || done != nil { + // To avoid deadlocking we need to ask the message handler to + // consume the data + handleMsg() + } + + if done != nil { + c.setReadTimeout(literalReadTimeout) + } + items <- item + if done != nil { + <-done + c.setReadTimeout(respReadTimeout) + } + return nil + }) +} + +func isMsgAttNameChar(ch byte) bool { + return ch != '[' && imapwire.IsAtomChar(ch) +} + +func readEnvelope(dec *imapwire.Decoder, options *Options) (*imap.Envelope, error) { + var envelope imap.Envelope + + if !dec.ExpectSpecial('(') { + return nil, dec.Err() + } + + var date, subject string + if !dec.ExpectNString(&date) || !dec.ExpectSP() || !dec.ExpectNString(&subject) || !dec.ExpectSP() { + return nil, dec.Err() + } + // TODO: handle error + envelope.Date, _ = netmail.ParseDate(date) + envelope.Subject, _ = options.decodeText(subject) + + addrLists := []struct { + name string + out *[]imap.Address + }{ + {"env-from", &envelope.From}, + {"env-sender", &envelope.Sender}, + {"env-reply-to", &envelope.ReplyTo}, + {"env-to", &envelope.To}, + {"env-cc", &envelope.Cc}, + {"env-bcc", &envelope.Bcc}, + } + for _, addrList := range addrLists { + l, err := readAddressList(dec, options) + if err != nil { + return nil, fmt.Errorf("in %v: %v", addrList.name, err) + } else if !dec.ExpectSP() { + return nil, dec.Err() + } + *addrList.out = l + } + + var inReplyTo, messageID string + if !dec.ExpectNString(&inReplyTo) || !dec.ExpectSP() || !dec.ExpectNString(&messageID) { + return nil, dec.Err() + } + // TODO: handle errors + envelope.InReplyTo, _ = parseMsgIDList(inReplyTo) + envelope.MessageID, _ = parseMsgID(messageID) + + if !dec.ExpectSpecial(')') { + return nil, dec.Err() + } + return &envelope, nil +} + +func readAddressList(dec *imapwire.Decoder, options *Options) ([]imap.Address, error) { + var l []imap.Address + err := dec.ExpectNList(func() error { + addr, err := readAddress(dec, options) + if err != nil { + return err + } + l = append(l, *addr) + return nil + }) + return l, err +} + +func readAddress(dec *imapwire.Decoder, options *Options) (*imap.Address, error) { + var ( + addr imap.Address + name string + obsRoute string + ) + ok := dec.ExpectSpecial('(') && + dec.ExpectNString(&name) && dec.ExpectSP() && + dec.ExpectNString(&obsRoute) && dec.ExpectSP() && + dec.ExpectNString(&addr.Mailbox) && dec.ExpectSP() && + dec.ExpectNString(&addr.Host) && dec.ExpectSpecial(')') + if !ok { + return nil, fmt.Errorf("in address: %v", dec.Err()) + } + // TODO: handle error + addr.Name, _ = options.decodeText(name) + return &addr, nil +} + +func parseMsgID(s string) (string, error) { + var h mail.Header + h.Set("Message-Id", s) + return h.MessageID() +} + +func parseMsgIDList(s string) ([]string, error) { + var h mail.Header + h.Set("In-Reply-To", s) + return h.MsgIDList("In-Reply-To") +} + +func readBody(dec *imapwire.Decoder, options *Options) (imap.BodyStructure, error) { + if !dec.ExpectSpecial('(') { + return nil, dec.Err() + } + + var ( + mediaType string + token string + bs imap.BodyStructure + err error + ) + if dec.String(&mediaType) { + token = "body-type-1part" + bs, err = readBodyType1part(dec, mediaType, options) + } else { + token = "body-type-mpart" + bs, err = readBodyTypeMpart(dec, options) + } + if err != nil { + return nil, fmt.Errorf("in %v: %v", token, err) + } + + for dec.SP() { + if !dec.DiscardValue() { + return nil, dec.Err() + } + } + + if !dec.ExpectSpecial(')') { + return nil, dec.Err() + } + + return bs, nil +} + +func readBodyType1part(dec *imapwire.Decoder, typ string, options *Options) (*imap.BodyStructureSinglePart, error) { + bs := imap.BodyStructureSinglePart{Type: typ} + + if !dec.ExpectSP() || !dec.ExpectString(&bs.Subtype) || !dec.ExpectSP() { + return nil, dec.Err() + } + var err error + bs.Params, err = readBodyFldParam(dec, options) + if err != nil { + return nil, err + } + + var description string + if !dec.ExpectSP() || !dec.ExpectNString(&bs.ID) || !dec.ExpectSP() || !dec.ExpectNString(&description) || !dec.ExpectSP() || !dec.ExpectNString(&bs.Encoding) || !dec.ExpectSP() || !dec.ExpectBodyFldOctets(&bs.Size) { + return nil, dec.Err() + } + + // Content-Transfer-Encoding should always be set, but some non-standard + // servers leave it NIL. Default to 7BIT. + if bs.Encoding == "" { + bs.Encoding = "7BIT" + } + + // TODO: handle errors + bs.Description, _ = options.decodeText(description) + + // Some servers don't include the extra fields for message and text + // (see https://github.com/emersion/go-imap/issues/557) + hasSP := dec.SP() + if !hasSP { + return &bs, nil + } + + if strings.EqualFold(bs.Type, "message") && (strings.EqualFold(bs.Subtype, "rfc822") || strings.EqualFold(bs.Subtype, "global")) { + var msg imap.BodyStructureMessageRFC822 + + msg.Envelope, err = readEnvelope(dec, options) + if err != nil { + return nil, err + } + + if !dec.ExpectSP() { + return nil, dec.Err() + } + + msg.BodyStructure, err = readBody(dec, options) + if err != nil { + return nil, err + } + + if !dec.ExpectSP() || !dec.ExpectNumber64(&msg.NumLines) { + return nil, dec.Err() + } + + bs.MessageRFC822 = &msg + hasSP = false + } else if strings.EqualFold(bs.Type, "text") { + var text imap.BodyStructureText + + if !dec.ExpectNumber64(&text.NumLines) { + return nil, dec.Err() + } + + bs.Text = &text + hasSP = false + } + + if !hasSP { + hasSP = dec.SP() + } + if hasSP { + bs.Extended, err = readBodyExt1part(dec, options) + if err != nil { + return nil, fmt.Errorf("in body-ext-1part: %v", err) + } + } + + return &bs, nil +} + +func readBodyExt1part(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureSinglePartExt, error) { + var ext imap.BodyStructureSinglePartExt + + var md5 string + if !dec.ExpectNString(&md5) { + return nil, dec.Err() + } + + if !dec.SP() { + return &ext, nil + } + + var err error + ext.Disposition, err = readBodyFldDsp(dec, options) + if err != nil { + return nil, fmt.Errorf("in body-fld-dsp: %v", err) + } + + if !dec.SP() { + return &ext, nil + } + + ext.Language, err = readBodyFldLang(dec) + if err != nil { + return nil, fmt.Errorf("in body-fld-lang: %v", err) + } + + if !dec.SP() { + return &ext, nil + } + + if !dec.ExpectNString(&ext.Location) { + return nil, dec.Err() + } + + return &ext, nil +} + +func readBodyTypeMpart(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureMultiPart, error) { + var bs imap.BodyStructureMultiPart + + for { + child, err := readBody(dec, options) + if err != nil { + return nil, err + } + bs.Children = append(bs.Children, child) + + if dec.SP() && dec.String(&bs.Subtype) { + break + } + } + + if dec.SP() { + var err error + bs.Extended, err = readBodyExtMpart(dec, options) + if err != nil { + return nil, fmt.Errorf("in body-ext-mpart: %v", err) + } + } + + return &bs, nil +} + +func readBodyExtMpart(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureMultiPartExt, error) { + var ext imap.BodyStructureMultiPartExt + + var err error + ext.Params, err = readBodyFldParam(dec, options) + if err != nil { + return nil, fmt.Errorf("in body-fld-param: %v", err) + } + + if !dec.SP() { + return &ext, nil + } + + ext.Disposition, err = readBodyFldDsp(dec, options) + if err != nil { + return nil, fmt.Errorf("in body-fld-dsp: %v", err) + } + + if !dec.SP() { + return &ext, nil + } + + ext.Language, err = readBodyFldLang(dec) + if err != nil { + return nil, fmt.Errorf("in body-fld-lang: %v", err) + } + + if !dec.SP() { + return &ext, nil + } + + if !dec.ExpectNString(&ext.Location) { + return nil, dec.Err() + } + + return &ext, nil +} + +func readBodyFldDsp(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureDisposition, error) { + if !dec.Special('(') { + if !dec.ExpectNIL() { + return nil, dec.Err() + } + return nil, nil + } + + var disp imap.BodyStructureDisposition + if !dec.ExpectString(&disp.Value) || !dec.ExpectSP() { + return nil, dec.Err() + } + + var err error + disp.Params, err = readBodyFldParam(dec, options) + if err != nil { + return nil, err + } + if !dec.ExpectSpecial(')') { + return nil, dec.Err() + } + return &disp, nil +} + +func readBodyFldParam(dec *imapwire.Decoder, options *Options) (map[string]string, error) { + var ( + params map[string]string + k string + ) + err := dec.ExpectNList(func() error { + var s string + if !dec.ExpectString(&s) { + return dec.Err() + } + + if k == "" { + k = s + } else { + if params == nil { + params = make(map[string]string) + } + decoded, _ := options.decodeText(s) + // TODO: handle error + + params[strings.ToLower(k)] = decoded + k = "" + } + + return nil + }) + if err != nil { + return nil, err + } else if k != "" { + return nil, fmt.Errorf("in body-fld-param: key without value") + } + return params, nil +} + +func readBodyFldLang(dec *imapwire.Decoder) ([]string, error) { + var l []string + isList, err := dec.List(func() error { + var s string + if !dec.ExpectString(&s) { + return dec.Err() + } + l = append(l, s) + return nil + }) + if err != nil || isList { + return l, err + } + + var s string + if !dec.ExpectNString(&s) { + return nil, dec.Err() + } + if s != "" { + return []string{s}, nil + } else { + return nil, nil + } +} + +func readSectionSpec(dec *imapwire.Decoder) (*imap.FetchItemBodySection, error) { + var section imap.FetchItemBodySection + + var dot bool + section.Part, dot = readSectionPart(dec) + if dot || len(section.Part) == 0 { + var specifier string + if dot { + if !dec.ExpectAtom(&specifier) { + return nil, dec.Err() + } + } else { + dec.Atom(&specifier) + } + specifier = strings.ToUpper(specifier) + section.Specifier = imap.PartSpecifier(specifier) + + if specifier == "HEADER.FIELDS" || specifier == "HEADER.FIELDS.NOT" { + if !dec.ExpectSP() { + return nil, dec.Err() + } + var err error + headerList, err := readHeaderList(dec) + if err != nil { + return nil, err + } + section.Specifier = imap.PartSpecifierHeader + if specifier == "HEADER.FIELDS" { + section.HeaderFields = headerList + } else { + section.HeaderFieldsNot = headerList + } + } + } + + if !dec.ExpectSpecial(']') { + return nil, dec.Err() + } + + offset, err := readPartialOffset(dec) + if err != nil { + return nil, err + } + if offset != nil { + section.Partial = &imap.SectionPartial{Offset: int64(*offset)} + } + + return §ion, nil +} + +func readPartialOffset(dec *imapwire.Decoder) (*uint32, error) { + if !dec.Special('<') { + return nil, nil + } + var offset uint32 + if !dec.ExpectNumber(&offset) || !dec.ExpectSpecial('>') { + return nil, dec.Err() + } + return &offset, nil +} + +func readHeaderList(dec *imapwire.Decoder) ([]string, error) { + var l []string + err := dec.ExpectList(func() error { + var s string + if !dec.ExpectAString(&s) { + return dec.Err() + } + l = append(l, s) + return nil + }) + return l, err +} + +func readSectionPart(dec *imapwire.Decoder) (part []int, dot bool) { + for { + dot = len(part) > 0 + if dot && !dec.Special('.') { + return part, false + } + + var num uint32 + if !dec.Number(&num) { + return part, dot + } + part = append(part, int(num)) + } +} + +type fetchLiteralReader struct { + *imapwire.LiteralReader + ch chan<- struct{} +} + +func (lit *fetchLiteralReader) Read(b []byte) (int, error) { + n, err := lit.LiteralReader.Read(b) + if err == io.EOF && lit.ch != nil { + close(lit.ch) + lit.ch = nil + } + return n, err +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/id.go b/vendor/github.com/emersion/go-imap/v2/imapclient/id.go new file mode 100644 index 000000000..0c10d6057 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/id.go @@ -0,0 +1,163 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// ID sends an ID command. +// +// The ID command is introduced in RFC 2971. It requires support for the ID +// extension. +// +// An example ID command: +// +// ID ("name" "go-imap" "version" "1.0" "os" "Linux" "os-version" "7.9.4" "vendor" "Yahoo") +func (c *Client) ID(idData *imap.IDData) *IDCommand { + cmd := &IDCommand{} + enc := c.beginCommand("ID", cmd) + + if idData == nil { + enc.SP().NIL() + enc.end() + return cmd + } + + enc.SP().Special('(') + isFirstKey := true + if idData.Name != "" { + addIDKeyValue(enc, &isFirstKey, "name", idData.Name) + } + if idData.Version != "" { + addIDKeyValue(enc, &isFirstKey, "version", idData.Version) + } + if idData.OS != "" { + addIDKeyValue(enc, &isFirstKey, "os", idData.OS) + } + if idData.OSVersion != "" { + addIDKeyValue(enc, &isFirstKey, "os-version", idData.OSVersion) + } + if idData.Vendor != "" { + addIDKeyValue(enc, &isFirstKey, "vendor", idData.Vendor) + } + if idData.SupportURL != "" { + addIDKeyValue(enc, &isFirstKey, "support-url", idData.SupportURL) + } + if idData.Address != "" { + addIDKeyValue(enc, &isFirstKey, "address", idData.Address) + } + if idData.Date != "" { + addIDKeyValue(enc, &isFirstKey, "date", idData.Date) + } + if idData.Command != "" { + addIDKeyValue(enc, &isFirstKey, "command", idData.Command) + } + if idData.Arguments != "" { + addIDKeyValue(enc, &isFirstKey, "arguments", idData.Arguments) + } + if idData.Environment != "" { + addIDKeyValue(enc, &isFirstKey, "environment", idData.Environment) + } + + enc.Special(')') + enc.end() + return cmd +} + +func addIDKeyValue(enc *commandEncoder, isFirstKey *bool, key, value string) { + if isFirstKey == nil { + panic("isFirstKey cannot be nil") + } else if !*isFirstKey { + enc.SP().Quoted(key).SP().Quoted(value) + } else { + enc.Quoted(key).SP().Quoted(value) + } + *isFirstKey = false +} + +func (c *Client) handleID() error { + data, err := c.readID(c.dec) + if err != nil { + return fmt.Errorf("in id: %v", err) + } + + if cmd := findPendingCmdByType[*IDCommand](c); cmd != nil { + cmd.data = *data + } + + return nil +} + +func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) { + var data = imap.IDData{} + + if !dec.ExpectSP() { + return nil, dec.Err() + } + + if dec.ExpectNIL() { + return &data, nil + } + + currKey := "" + err := dec.ExpectList(func() error { + var keyOrValue string + if !dec.String(&keyOrValue) { + return fmt.Errorf("in id key-val list: %v", dec.Err()) + } + + if currKey == "" { + currKey = keyOrValue + return nil + } + + switch currKey { + case "name": + data.Name = keyOrValue + case "version": + data.Version = keyOrValue + case "os": + data.OS = keyOrValue + case "os-version": + data.OSVersion = keyOrValue + case "vendor": + data.Vendor = keyOrValue + case "support-url": + data.SupportURL = keyOrValue + case "address": + data.Address = keyOrValue + case "date": + data.Date = keyOrValue + case "command": + data.Command = keyOrValue + case "arguments": + data.Arguments = keyOrValue + case "environment": + data.Environment = keyOrValue + default: + // Ignore unknown key + // Yahoo server sends "host" and "remote-host" keys + // which are not defined in RFC 2971 + } + currKey = "" + + return nil + }) + + if err != nil { + return nil, err + } + + return &data, nil +} + +type IDCommand struct { + commandBase + data imap.IDData +} + +func (r *IDCommand) Wait() (*imap.IDData, error) { + return &r.data, r.wait() +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/idle.go b/vendor/github.com/emersion/go-imap/v2/imapclient/idle.go new file mode 100644 index 000000000..1613bff33 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/idle.go @@ -0,0 +1,157 @@ +package imapclient + +import ( + "fmt" + "sync/atomic" + "time" +) + +const idleRestartInterval = 28 * time.Minute + +// Idle sends an IDLE command. +// +// Unlike other commands, this method blocks until the server acknowledges it. +// On success, the IDLE command is running and other commands cannot be sent. +// The caller must invoke IdleCommand.Close to stop IDLE and unblock the +// client. +// +// This command requires support for IMAP4rev2 or the IDLE extension. The IDLE +// command is restarted automatically to avoid getting disconnected due to +// inactivity timeouts. +func (c *Client) Idle() (*IdleCommand, error) { + child, err := c.idle() + if err != nil { + return nil, err + } + + cmd := &IdleCommand{ + stop: make(chan struct{}), + done: make(chan struct{}), + } + go cmd.run(c, child) + return cmd, nil +} + +// IdleCommand is an IDLE command. +// +// Initially, the IDLE command is running. The server may send unilateral +// data. The client cannot send any command while IDLE is running. +// +// Close must be called to stop the IDLE command. +type IdleCommand struct { + stopped atomic.Bool + stop chan struct{} + done chan struct{} + + err error + lastChild *idleCommand +} + +func (cmd *IdleCommand) run(c *Client, child *idleCommand) { + defer close(cmd.done) + + timer := time.NewTimer(idleRestartInterval) + defer timer.Stop() + + defer func() { + if child != nil { + if err := child.Close(); err != nil && cmd.err == nil { + cmd.err = err + } + } + }() + + for { + select { + case <-timer.C: + timer.Reset(idleRestartInterval) + + if cmd.err = child.Close(); cmd.err != nil { + return + } + if child, cmd.err = c.idle(); cmd.err != nil { + return + } + case <-c.decCh: + cmd.lastChild = child + return + case <-cmd.stop: + cmd.lastChild = child + return + } + } +} + +// Close stops the IDLE command. +// +// This method blocks until the command to stop IDLE is written, but doesn't +// wait for the server to respond. Callers can use Wait for this purpose. +func (cmd *IdleCommand) Close() error { + if cmd.stopped.Swap(true) { + return fmt.Errorf("imapclient: IDLE already closed") + } + close(cmd.stop) + <-cmd.done + return cmd.err +} + +// Wait blocks until the IDLE command has completed. +func (cmd *IdleCommand) Wait() error { + <-cmd.done + if cmd.err != nil { + return cmd.err + } + return cmd.lastChild.Wait() +} + +func (c *Client) idle() (*idleCommand, error) { + cmd := &idleCommand{} + contReq := c.registerContReq(cmd) + cmd.enc = c.beginCommand("IDLE", cmd) + cmd.enc.flush() + + _, err := contReq.Wait() + if err != nil { + cmd.enc.end() + return nil, err + } + + return cmd, nil +} + +// idleCommand represents a singular IDLE command, without the restart logic. +type idleCommand struct { + commandBase + enc *commandEncoder +} + +// Close stops the IDLE command. +// +// This method blocks until the command to stop IDLE is written, but doesn't +// wait for the server to respond. Callers can use Wait for this purpose. +func (cmd *idleCommand) Close() error { + if cmd.err != nil { + return cmd.err + } + if cmd.enc == nil { + return fmt.Errorf("imapclient: IDLE command closed twice") + } + cmd.enc.client.setWriteTimeout(cmdWriteTimeout) + _, err := cmd.enc.client.bw.WriteString("DONE\r\n") + if err == nil { + err = cmd.enc.client.bw.Flush() + } + cmd.enc.end() + cmd.enc = nil + return err +} + +// Wait blocks until the IDLE command has completed. +// +// Wait can only be called after Close. +func (cmd *idleCommand) Wait() error { + if cmd.enc != nil { + panic("imapclient: idleCommand.Close must be called before Wait") + } + return cmd.wait() +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/list.go b/vendor/github.com/emersion/go-imap/v2/imapclient/list.go new file mode 100644 index 000000000..2c0ce1680 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/list.go @@ -0,0 +1,259 @@ +package imapclient + +import ( + "fmt" + "strings" + "unicode/utf8" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +func getSelectOpts(options *imap.ListOptions) []string { + if options == nil { + return nil + } + + var l []string + if options.SelectSubscribed { + l = append(l, "SUBSCRIBED") + } + if options.SelectRemote { + l = append(l, "REMOTE") + } + if options.SelectRecursiveMatch { + l = append(l, "RECURSIVEMATCH") + } + if options.SelectSpecialUse { + l = append(l, "SPECIAL-USE") + } + return l +} + +func getReturnOpts(options *imap.ListOptions) []string { + if options == nil { + return nil + } + + var l []string + if options.ReturnSubscribed { + l = append(l, "SUBSCRIBED") + } + if options.ReturnChildren { + l = append(l, "CHILDREN") + } + if options.ReturnStatus != nil { + l = append(l, "STATUS") + } + if options.ReturnSpecialUse { + l = append(l, "SPECIAL-USE") + } + return l +} + +// List sends a LIST command. +// +// The caller must fully consume the ListCommand. A simple way to do so is to +// defer a call to ListCommand.Close. +// +// A nil options pointer is equivalent to a zero options value. +// +// A non-zero options value requires support for IMAP4rev2 or the LIST-EXTENDED +// extension. +func (c *Client) List(ref, pattern string, options *imap.ListOptions) *ListCommand { + cmd := &ListCommand{ + mailboxes: make(chan *imap.ListData, 64), + returnStatus: options != nil && options.ReturnStatus != nil, + } + enc := c.beginCommand("LIST", cmd) + if selectOpts := getSelectOpts(options); len(selectOpts) > 0 { + enc.SP().List(len(selectOpts), func(i int) { + enc.Atom(selectOpts[i]) + }) + } + enc.SP().Mailbox(ref).SP().Mailbox(pattern) + if returnOpts := getReturnOpts(options); len(returnOpts) > 0 { + enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) { + opt := returnOpts[i] + enc.Atom(opt) + if opt == "STATUS" { + returnStatus := statusItems(options.ReturnStatus) + enc.SP().List(len(returnStatus), func(j int) { + enc.Atom(returnStatus[j]) + }) + } + }) + } + enc.end() + return cmd +} + +func (c *Client) handleList() error { + data, err := readList(c.dec) + if err != nil { + return fmt.Errorf("in LIST: %v", err) + } + + cmd := c.findPendingCmdFunc(func(cmd command) bool { + switch cmd := cmd.(type) { + case *ListCommand: + return true // TODO: match pattern, check if already handled + case *SelectCommand: + return cmd.mailbox == data.Mailbox && cmd.data.List == nil + default: + return false + } + }) + switch cmd := cmd.(type) { + case *ListCommand: + if cmd.returnStatus { + if cmd.pendingData != nil { + cmd.mailboxes <- cmd.pendingData + } + cmd.pendingData = data + } else { + cmd.mailboxes <- data + } + case *SelectCommand: + cmd.data.List = data + } + + return nil +} + +// ListCommand is a LIST command. +type ListCommand struct { + commandBase + mailboxes chan *imap.ListData + + returnStatus bool + pendingData *imap.ListData +} + +// Next advances to the next mailbox. +// +// On success, the mailbox LIST data is returned. On error or if there are no +// more mailboxes, nil is returned. +func (cmd *ListCommand) Next() *imap.ListData { + return <-cmd.mailboxes +} + +// Close releases the command. +// +// Calling Close unblocks the IMAP client decoder and lets it read the next +// responses. Next will always return nil after Close. +func (cmd *ListCommand) Close() error { + for cmd.Next() != nil { + // ignore + } + return cmd.wait() +} + +// Collect accumulates mailboxes into a list. +// +// This is equivalent to calling Next repeatedly and then Close. +func (cmd *ListCommand) Collect() ([]*imap.ListData, error) { + var l []*imap.ListData + for { + data := cmd.Next() + if data == nil { + break + } + l = append(l, data) + } + return l, cmd.Close() +} + +func readList(dec *imapwire.Decoder) (*imap.ListData, error) { + var data imap.ListData + + var err error + data.Attrs, err = internal.ExpectMailboxAttrList(dec) + if err != nil { + return nil, fmt.Errorf("in mbx-list-flags: %w", err) + } + + if !dec.ExpectSP() { + return nil, dec.Err() + } + + data.Delim, err = readDelim(dec) + if err != nil { + return nil, err + } + + if !dec.ExpectSP() || !dec.ExpectMailbox(&data.Mailbox) { + return nil, dec.Err() + } + + if dec.SP() { + err := dec.ExpectList(func() error { + var tag string + if !dec.ExpectAString(&tag) || !dec.ExpectSP() { + return dec.Err() + } + var err error + switch strings.ToUpper(tag) { + case "CHILDINFO": + data.ChildInfo, err = readChildInfoExtendedItem(dec) + if err != nil { + return fmt.Errorf("in childinfo-extended-item: %v", err) + } + case "OLDNAME": + data.OldName, err = readOldNameExtendedItem(dec) + if err != nil { + return fmt.Errorf("in oldname-extended-item: %v", err) + } + default: + if !dec.DiscardValue() { + return fmt.Errorf("in tagged-ext-val: %v", err) + } + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("in mbox-list-extended: %v", err) + } + } + + return &data, nil +} + +func readChildInfoExtendedItem(dec *imapwire.Decoder) (*imap.ListDataChildInfo, error) { + var childInfo imap.ListDataChildInfo + err := dec.ExpectList(func() error { + var opt string + if !dec.ExpectAString(&opt) { + return dec.Err() + } + if strings.ToUpper(opt) == "SUBSCRIBED" { + childInfo.Subscribed = true + } + return nil + }) + return &childInfo, err +} + +func readOldNameExtendedItem(dec *imapwire.Decoder) (string, error) { + var name string + if !dec.ExpectSpecial('(') || !dec.ExpectMailbox(&name) || !dec.ExpectSpecial(')') { + return "", dec.Err() + } + return name, nil +} + +func readDelim(dec *imapwire.Decoder) (rune, error) { + var delimStr string + if dec.Quoted(&delimStr) { + delim, size := utf8.DecodeRuneInString(delimStr) + if delim == utf8.RuneError || size != len(delimStr) { + return 0, fmt.Errorf("mailbox delimiter must be a single rune") + } + return delim, nil + } else if !dec.ExpectNIL() { + return 0, dec.Err() + } else { + return 0, nil + } +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/metadata.go b/vendor/github.com/emersion/go-imap/v2/imapclient/metadata.go new file mode 100644 index 000000000..c8a0e7282 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/metadata.go @@ -0,0 +1,205 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +type GetMetadataDepth int + +const ( + GetMetadataDepthZero GetMetadataDepth = 0 + GetMetadataDepthOne GetMetadataDepth = 1 + GetMetadataDepthInfinity GetMetadataDepth = -1 +) + +func (depth GetMetadataDepth) String() string { + switch depth { + case GetMetadataDepthZero: + return "0" + case GetMetadataDepthOne: + return "1" + case GetMetadataDepthInfinity: + return "infinity" + default: + panic(fmt.Errorf("imapclient: unknown GETMETADATA depth %d", depth)) + } +} + +// GetMetadataOptions contains options for the GETMETADATA command. +type GetMetadataOptions struct { + MaxSize *uint32 + Depth GetMetadataDepth +} + +func (options *GetMetadataOptions) names() []string { + if options == nil { + return nil + } + var l []string + if options.MaxSize != nil { + l = append(l, "MAXSIZE") + } + if options.Depth != GetMetadataDepthZero { + l = append(l, "DEPTH") + } + return l +} + +// GetMetadata sends a GETMETADATA command. +// +// This command requires support for the METADATA or METADATA-SERVER extension. +func (c *Client) GetMetadata(mailbox string, entries []string, options *GetMetadataOptions) *GetMetadataCommand { + cmd := &GetMetadataCommand{mailbox: mailbox} + enc := c.beginCommand("GETMETADATA", cmd) + enc.SP().Mailbox(mailbox) + if opts := options.names(); len(opts) > 0 { + enc.SP().List(len(opts), func(i int) { + opt := opts[i] + enc.Atom(opt).SP() + switch opt { + case "MAXSIZE": + enc.Number(*options.MaxSize) + case "DEPTH": + enc.Atom(options.Depth.String()) + default: + panic(fmt.Errorf("imapclient: unknown GETMETADATA option %q", opt)) + } + }) + } + enc.SP().List(len(entries), func(i int) { + enc.String(entries[i]) + }) + enc.end() + return cmd +} + +// SetMetadata sends a SETMETADATA command. +// +// To remove an entry, set it to nil. +// +// This command requires support for the METADATA or METADATA-SERVER extension. +func (c *Client) SetMetadata(mailbox string, entries map[string]*[]byte) *Command { + cmd := &Command{} + enc := c.beginCommand("SETMETADATA", cmd) + enc.SP().Mailbox(mailbox).SP().Special('(') + i := 0 + for k, v := range entries { + if i > 0 { + enc.SP() + } + enc.String(k).SP() + if v == nil { + enc.NIL() + } else { + enc.String(string(*v)) // TODO: use literals if required + } + i++ + } + enc.Special(')') + enc.end() + return cmd +} + +func (c *Client) handleMetadata() error { + data, err := readMetadataResp(c.dec) + if err != nil { + return fmt.Errorf("in metadata-resp: %v", err) + } + + cmd := c.findPendingCmdFunc(func(anyCmd command) bool { + cmd, ok := anyCmd.(*GetMetadataCommand) + return ok && cmd.mailbox == data.Mailbox + }) + if cmd != nil && len(data.EntryValues) > 0 { + cmd := cmd.(*GetMetadataCommand) + cmd.data.Mailbox = data.Mailbox + if cmd.data.Entries == nil { + cmd.data.Entries = make(map[string]*[]byte) + } + // The server might send multiple METADATA responses for a single + // METADATA command + for k, v := range data.EntryValues { + cmd.data.Entries[k] = v + } + } else if handler := c.options.unilateralDataHandler().Metadata; handler != nil && len(data.EntryList) > 0 { + handler(data.Mailbox, data.EntryList) + } + + return nil +} + +// GetMetadataCommand is a GETMETADATA command. +type GetMetadataCommand struct { + commandBase + mailbox string + data GetMetadataData +} + +func (cmd *GetMetadataCommand) Wait() (*GetMetadataData, error) { + return &cmd.data, cmd.wait() +} + +// GetMetadataData is the data returned by the GETMETADATA command. +type GetMetadataData struct { + Mailbox string + Entries map[string]*[]byte +} + +type metadataResp struct { + Mailbox string + EntryList []string + EntryValues map[string]*[]byte +} + +func readMetadataResp(dec *imapwire.Decoder) (*metadataResp, error) { + var data metadataResp + + if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() { + return nil, dec.Err() + } + + isList, err := dec.List(func() error { + var name string + if !dec.ExpectAString(&name) || !dec.ExpectSP() { + return dec.Err() + } + + // TODO: decode as []byte + var ( + value *[]byte + s string + ) + if dec.String(&s) || dec.Literal(&s) { + b := []byte(s) + value = &b + } else if !dec.ExpectNIL() { + return dec.Err() + } + + if data.EntryValues == nil { + data.EntryValues = make(map[string]*[]byte) + } + data.EntryValues[name] = value + return nil + }) + if err != nil { + return nil, err + } else if !isList { + var name string + if !dec.ExpectAString(&name) { + return nil, dec.Err() + } + data.EntryList = append(data.EntryList, name) + + for dec.SP() { + if !dec.ExpectAString(&name) { + return nil, dec.Err() + } + data.EntryList = append(data.EntryList, name) + } + } + + return &data, nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/move.go b/vendor/github.com/emersion/go-imap/v2/imapclient/move.go new file mode 100644 index 000000000..6fa0b62e2 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/move.go @@ -0,0 +1,74 @@ +package imapclient + +import ( + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// Move sends a MOVE command. +// +// If the server doesn't support IMAP4rev2 nor the MOVE extension, a fallback +// with COPY + STORE + EXPUNGE commands is used. +func (c *Client) Move(numSet imap.NumSet, mailbox string) *MoveCommand { + // If the server doesn't support MOVE, fallback to [UID] COPY, + // [UID] STORE +FLAGS.SILENT \Deleted and [UID] EXPUNGE + cmdName := "MOVE" + if !c.Caps().Has(imap.CapMove) { + cmdName = "COPY" + } + + cmd := &MoveCommand{} + enc := c.beginCommand(uidCmdName(cmdName, imapwire.NumSetKind(numSet)), cmd) + enc.SP().NumSet(numSet).SP().Mailbox(mailbox) + enc.end() + + if cmdName == "COPY" { + cmd.store = c.Store(numSet, &imap.StoreFlags{ + Op: imap.StoreFlagsAdd, + Silent: true, + Flags: []imap.Flag{imap.FlagDeleted}, + }, nil) + if uidSet, ok := numSet.(imap.UIDSet); ok && c.Caps().Has(imap.CapUIDPlus) { + cmd.expunge = c.UIDExpunge(uidSet) + } else { + cmd.expunge = c.Expunge() + } + } + + return cmd +} + +// MoveCommand is a MOVE command. +type MoveCommand struct { + commandBase + data MoveData + + // Fallback + store *FetchCommand + expunge *ExpungeCommand +} + +func (cmd *MoveCommand) Wait() (*MoveData, error) { + if err := cmd.wait(); err != nil { + return nil, err + } + if cmd.store != nil { + if err := cmd.store.Close(); err != nil { + return nil, err + } + } + if cmd.expunge != nil { + if err := cmd.expunge.Close(); err != nil { + return nil, err + } + } + return &cmd.data, nil +} + +// MoveData contains the data returned by a MOVE command. +type MoveData struct { + // requires UIDPLUS or IMAP4rev2 + UIDValidity uint32 + SourceUIDs imap.NumSet + DestUIDs imap.NumSet +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/namespace.go b/vendor/github.com/emersion/go-imap/v2/imapclient/namespace.go new file mode 100644 index 000000000..8c4738ea5 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/namespace.go @@ -0,0 +1,110 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// Namespace sends a NAMESPACE command. +// +// This command requires support for IMAP4rev2 or the NAMESPACE extension. +func (c *Client) Namespace() *NamespaceCommand { + cmd := &NamespaceCommand{} + c.beginCommand("NAMESPACE", cmd).end() + return cmd +} + +func (c *Client) handleNamespace() error { + data, err := readNamespaceResponse(c.dec) + if err != nil { + return fmt.Errorf("in namespace-response: %v", err) + } + if cmd := findPendingCmdByType[*NamespaceCommand](c); cmd != nil { + cmd.data = *data + } + return nil +} + +// NamespaceCommand is a NAMESPACE command. +type NamespaceCommand struct { + commandBase + data imap.NamespaceData +} + +func (cmd *NamespaceCommand) Wait() (*imap.NamespaceData, error) { + return &cmd.data, cmd.wait() +} + +func readNamespaceResponse(dec *imapwire.Decoder) (*imap.NamespaceData, error) { + var ( + data imap.NamespaceData + err error + ) + + data.Personal, err = readNamespace(dec) + if err != nil { + return nil, err + } + + if !dec.ExpectSP() { + return nil, dec.Err() + } + + data.Other, err = readNamespace(dec) + if err != nil { + return nil, err + } + + if !dec.ExpectSP() { + return nil, dec.Err() + } + + data.Shared, err = readNamespace(dec) + if err != nil { + return nil, err + } + + return &data, nil +} + +func readNamespace(dec *imapwire.Decoder) ([]imap.NamespaceDescriptor, error) { + var l []imap.NamespaceDescriptor + err := dec.ExpectNList(func() error { + descr, err := readNamespaceDescr(dec) + if err != nil { + return fmt.Errorf("in namespace-descr: %v", err) + } + l = append(l, *descr) + return nil + }) + return l, err +} + +func readNamespaceDescr(dec *imapwire.Decoder) (*imap.NamespaceDescriptor, error) { + var descr imap.NamespaceDescriptor + + if !dec.ExpectSpecial('(') || !dec.ExpectString(&descr.Prefix) || !dec.ExpectSP() { + return nil, dec.Err() + } + + var err error + descr.Delim, err = readDelim(dec) + if err != nil { + return nil, err + } + + // Skip namespace-response-extensions + for dec.SP() { + if !dec.DiscardValue() { + return nil, dec.Err() + } + } + + if !dec.ExpectSpecial(')') { + return nil, dec.Err() + } + + return &descr, nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/quota.go b/vendor/github.com/emersion/go-imap/v2/imapclient/quota.go new file mode 100644 index 000000000..6775b9f64 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/quota.go @@ -0,0 +1,176 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// GetQuota sends a GETQUOTA command. +// +// This command requires support for the QUOTA extension. +func (c *Client) GetQuota(root string) *GetQuotaCommand { + cmd := &GetQuotaCommand{root: root} + enc := c.beginCommand("GETQUOTA", cmd) + enc.SP().String(root) + enc.end() + return cmd +} + +// GetQuotaRoot sends a GETQUOTAROOT command. +// +// This command requires support for the QUOTA extension. +func (c *Client) GetQuotaRoot(mailbox string) *GetQuotaRootCommand { + cmd := &GetQuotaRootCommand{mailbox: mailbox} + enc := c.beginCommand("GETQUOTAROOT", cmd) + enc.SP().Mailbox(mailbox) + enc.end() + return cmd +} + +// SetQuota sends a SETQUOTA command. +// +// This command requires support for the SETQUOTA extension. +func (c *Client) SetQuota(root string, limits map[imap.QuotaResourceType]int64) *Command { + // TODO: consider returning the QUOTA response data? + cmd := &Command{} + enc := c.beginCommand("SETQUOTA", cmd) + enc.SP().String(root).SP().Special('(') + i := 0 + for typ, limit := range limits { + if i > 0 { + enc.SP() + } + enc.Atom(string(typ)).SP().Number64(limit) + i++ + } + enc.Special(')') + enc.end() + return cmd +} + +func (c *Client) handleQuota() error { + data, err := readQuotaResponse(c.dec) + if err != nil { + return fmt.Errorf("in quota-response: %v", err) + } + + cmd := c.findPendingCmdFunc(func(cmd command) bool { + switch cmd := cmd.(type) { + case *GetQuotaCommand: + return cmd.root == data.Root + case *GetQuotaRootCommand: + for _, root := range cmd.roots { + if root == data.Root { + return true + } + } + return false + default: + return false + } + }) + switch cmd := cmd.(type) { + case *GetQuotaCommand: + cmd.data = data + case *GetQuotaRootCommand: + cmd.data = append(cmd.data, *data) + } + return nil +} + +func (c *Client) handleQuotaRoot() error { + mailbox, roots, err := readQuotaRoot(c.dec) + if err != nil { + return fmt.Errorf("in quotaroot-response: %v", err) + } + + cmd := c.findPendingCmdFunc(func(anyCmd command) bool { + cmd, ok := anyCmd.(*GetQuotaRootCommand) + if !ok { + return false + } + return cmd.mailbox == mailbox + }) + if cmd != nil { + cmd := cmd.(*GetQuotaRootCommand) + cmd.roots = roots + } + return nil +} + +// GetQuotaCommand is a GETQUOTA command. +type GetQuotaCommand struct { + commandBase + root string + data *QuotaData +} + +func (cmd *GetQuotaCommand) Wait() (*QuotaData, error) { + if err := cmd.wait(); err != nil { + return nil, err + } + return cmd.data, nil +} + +// GetQuotaRootCommand is a GETQUOTAROOT command. +type GetQuotaRootCommand struct { + commandBase + mailbox string + roots []string + data []QuotaData +} + +func (cmd *GetQuotaRootCommand) Wait() ([]QuotaData, error) { + if err := cmd.wait(); err != nil { + return nil, err + } + return cmd.data, nil +} + +// QuotaData is the data returned by a QUOTA response. +type QuotaData struct { + Root string + Resources map[imap.QuotaResourceType]QuotaResourceData +} + +// QuotaResourceData contains the usage and limit for a quota resource. +type QuotaResourceData struct { + Usage int64 + Limit int64 +} + +func readQuotaResponse(dec *imapwire.Decoder) (*QuotaData, error) { + var data QuotaData + if !dec.ExpectAString(&data.Root) || !dec.ExpectSP() { + return nil, dec.Err() + } + data.Resources = make(map[imap.QuotaResourceType]QuotaResourceData) + err := dec.ExpectList(func() error { + var ( + name string + resData QuotaResourceData + ) + if !dec.ExpectAtom(&name) || !dec.ExpectSP() || !dec.ExpectNumber64(&resData.Usage) || !dec.ExpectSP() || !dec.ExpectNumber64(&resData.Limit) { + return fmt.Errorf("in quota-resource: %v", dec.Err()) + } + data.Resources[imap.QuotaResourceType(name)] = resData + return nil + }) + return &data, err +} + +func readQuotaRoot(dec *imapwire.Decoder) (mailbox string, roots []string, err error) { + if !dec.ExpectMailbox(&mailbox) { + return "", nil, dec.Err() + } + for dec.SP() { + var root string + if !dec.ExpectAString(&root) { + return "", nil, dec.Err() + } + roots = append(roots, root) + } + return mailbox, roots, nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/search.go b/vendor/github.com/emersion/go-imap/v2/imapclient/search.go new file mode 100644 index 000000000..ee2b2b9bb --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/search.go @@ -0,0 +1,401 @@ +package imapclient + +import ( + "fmt" + "strings" + "time" + "unicode" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +func returnSearchOptions(options *imap.SearchOptions) []string { + if options == nil { + return nil + } + + m := map[string]bool{ + "MIN": options.ReturnMin, + "MAX": options.ReturnMax, + "ALL": options.ReturnAll, + "COUNT": options.ReturnCount, + } + + var l []string + for k, ret := range m { + if ret { + l = append(l, k) + } + } + return l +} + +func (c *Client) search(numKind imapwire.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand { + // The IMAP4rev2 SEARCH charset defaults to UTF-8. When UTF8=ACCEPT is + // enabled, specifying any CHARSET is invalid. For IMAP4rev1 the default is + // undefined and only US-ASCII support is required. What's more, some + // servers completely reject the CHARSET keyword. So, let's check if we + // actually have UTF-8 strings in the search criteria before using that. + // TODO: there might be a benefit in specifying CHARSET UTF-8 for IMAP4rev1 + // servers even if we only send ASCII characters: the server then must + // decode encoded headers and Content-Transfer-Encoding before matching the + // criteria. + var charset string + if !c.Caps().Has(imap.CapIMAP4rev2) && !c.enabled.Has(imap.CapUTF8Accept) && !searchCriteriaIsASCII(criteria) { + charset = "UTF-8" + } + + var all imap.NumSet + switch numKind { + case imapwire.NumKindSeq: + all = imap.SeqSet(nil) + case imapwire.NumKindUID: + all = imap.UIDSet(nil) + } + + cmd := &SearchCommand{} + cmd.data.All = all + enc := c.beginCommand(uidCmdName("SEARCH", numKind), cmd) + if returnOpts := returnSearchOptions(options); len(returnOpts) > 0 { + enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) { + enc.Atom(returnOpts[i]) + }) + } + enc.SP() + if charset != "" { + enc.Atom("CHARSET").SP().Atom(charset).SP() + } + writeSearchKey(enc.Encoder, criteria) + enc.end() + return cmd +} + +// Search sends a SEARCH command. +func (c *Client) Search(criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand { + return c.search(imapwire.NumKindSeq, criteria, options) +} + +// UIDSearch sends a UID SEARCH command. +func (c *Client) UIDSearch(criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand { + return c.search(imapwire.NumKindUID, criteria, options) +} + +func (c *Client) handleSearch() error { + cmd := findPendingCmdByType[*SearchCommand](c) + for c.dec.SP() { + if c.dec.Special('(') { + var name string + if !c.dec.ExpectAtom(&name) || !c.dec.ExpectSP() { + return c.dec.Err() + } else if strings.ToUpper(name) != "MODSEQ" { + return fmt.Errorf("in search-sort-mod-seq: expected %q, got %q", "MODSEQ", name) + } + var modSeq uint64 + if !c.dec.ExpectModSeq(&modSeq) || !c.dec.ExpectSpecial(')') { + return c.dec.Err() + } + if cmd != nil { + cmd.data.ModSeq = modSeq + } + break + } + + var num uint32 + if !c.dec.ExpectNumber(&num) { + return c.dec.Err() + } + if cmd != nil { + switch all := cmd.data.All.(type) { + case imap.SeqSet: + all.AddNum(num) + cmd.data.All = all + case imap.UIDSet: + all.AddNum(imap.UID(num)) + cmd.data.All = all + } + } + } + return nil +} + +func (c *Client) handleESearch() error { + if !c.dec.ExpectSP() { + return c.dec.Err() + } + tag, data, err := readESearchResponse(c.dec) + if err != nil { + return err + } + cmd := c.findPendingCmdFunc(func(anyCmd command) bool { + cmd, ok := anyCmd.(*SearchCommand) + if !ok { + return false + } + if tag != "" { + return cmd.tag == tag + } else { + return true + } + }) + if cmd != nil { + cmd := cmd.(*SearchCommand) + cmd.data = *data + } + return nil +} + +// SearchCommand is a SEARCH command. +type SearchCommand struct { + commandBase + data imap.SearchData +} + +func (cmd *SearchCommand) Wait() (*imap.SearchData, error) { + return &cmd.data, cmd.wait() +} + +func writeSearchKey(enc *imapwire.Encoder, criteria *imap.SearchCriteria) { + firstItem := true + encodeItem := func() *imapwire.Encoder { + if !firstItem { + enc.SP() + } + firstItem = false + return enc + } + + for _, seqSet := range criteria.SeqNum { + encodeItem().NumSet(seqSet) + } + for _, uidSet := range criteria.UID { + encodeItem().Atom("UID").SP().NumSet(uidSet) + } + + if !criteria.Since.IsZero() && !criteria.Before.IsZero() && criteria.Before.Sub(criteria.Since) == 24*time.Hour { + encodeItem().Atom("ON").SP().String(criteria.Since.Format(internal.DateLayout)) + } else { + if !criteria.Since.IsZero() { + encodeItem().Atom("SINCE").SP().String(criteria.Since.Format(internal.DateLayout)) + } + if !criteria.Before.IsZero() { + encodeItem().Atom("BEFORE").SP().String(criteria.Before.Format(internal.DateLayout)) + } + } + if !criteria.SentSince.IsZero() && !criteria.SentBefore.IsZero() && criteria.SentBefore.Sub(criteria.SentSince) == 24*time.Hour { + encodeItem().Atom("SENTON").SP().String(criteria.SentSince.Format(internal.DateLayout)) + } else { + if !criteria.SentSince.IsZero() { + encodeItem().Atom("SENTSINCE").SP().String(criteria.SentSince.Format(internal.DateLayout)) + } + if !criteria.SentBefore.IsZero() { + encodeItem().Atom("SENTBEFORE").SP().String(criteria.SentBefore.Format(internal.DateLayout)) + } + } + + for _, kv := range criteria.Header { + switch k := strings.ToUpper(kv.Key); k { + case "BCC", "CC", "FROM", "SUBJECT", "TO": + encodeItem().Atom(k) + default: + encodeItem().Atom("HEADER").SP().String(kv.Key) + } + enc.SP().String(kv.Value) + } + + for _, s := range criteria.Body { + encodeItem().Atom("BODY").SP().String(s) + } + for _, s := range criteria.Text { + encodeItem().Atom("TEXT").SP().String(s) + } + + for _, flag := range criteria.Flag { + if k := flagSearchKey(flag); k != "" { + encodeItem().Atom(k) + } else { + encodeItem().Atom("KEYWORD").SP().Flag(flag) + } + } + for _, flag := range criteria.NotFlag { + if k := flagSearchKey(flag); k != "" { + encodeItem().Atom("UN" + k) + } else { + encodeItem().Atom("UNKEYWORD").SP().Flag(flag) + } + } + + if criteria.Larger > 0 { + encodeItem().Atom("LARGER").SP().Number64(criteria.Larger) + } + if criteria.Smaller > 0 { + encodeItem().Atom("SMALLER").SP().Number64(criteria.Smaller) + } + + if modSeq := criteria.ModSeq; modSeq != nil { + encodeItem().Atom("MODSEQ") + if modSeq.MetadataName != "" && modSeq.MetadataType != "" { + enc.SP().Quoted(modSeq.MetadataName).SP().Atom(string(modSeq.MetadataType)) + } + enc.SP() + if modSeq.ModSeq != 0 { + enc.ModSeq(modSeq.ModSeq) + } else { + enc.Atom("0") + } + } + + for _, not := range criteria.Not { + encodeItem().Atom("NOT").SP() + enc.Special('(') + writeSearchKey(enc, ¬) + enc.Special(')') + } + for _, or := range criteria.Or { + encodeItem().Atom("OR").SP() + enc.Special('(') + writeSearchKey(enc, &or[0]) + enc.Special(')') + enc.SP() + enc.Special('(') + writeSearchKey(enc, &or[1]) + enc.Special(')') + } + + if firstItem { + enc.Atom("ALL") + } +} + +func flagSearchKey(flag imap.Flag) string { + switch flag { + case imap.FlagAnswered, imap.FlagDeleted, imap.FlagDraft, imap.FlagFlagged, imap.FlagSeen: + return strings.ToUpper(strings.TrimPrefix(string(flag), "\\")) + default: + return "" + } +} + +func readESearchResponse(dec *imapwire.Decoder) (tag string, data *imap.SearchData, err error) { + data = &imap.SearchData{} + if dec.Special('(') { // search-correlator + var correlator string + if !dec.ExpectAtom(&correlator) || !dec.ExpectSP() || !dec.ExpectAString(&tag) || !dec.ExpectSpecial(')') { + return "", nil, dec.Err() + } + if correlator != "TAG" { + return "", nil, fmt.Errorf("in search-correlator: name must be TAG, but got %q", correlator) + } + } + + var name string + if !dec.SP() { + return tag, data, nil + } else if !dec.ExpectAtom(&name) { + return "", nil, dec.Err() + } + data.UID = name == "UID" + + if data.UID { + if !dec.SP() { + return tag, data, nil + } else if !dec.ExpectAtom(&name) { + return "", nil, dec.Err() + } + } + + for { + if !dec.ExpectSP() { + return "", nil, dec.Err() + } + + switch strings.ToUpper(name) { + case "MIN": + var num uint32 + if !dec.ExpectNumber(&num) { + return "", nil, dec.Err() + } + data.Min = num + case "MAX": + var num uint32 + if !dec.ExpectNumber(&num) { + return "", nil, dec.Err() + } + data.Max = num + case "ALL": + numKind := imapwire.NumKindSeq + if data.UID { + numKind = imapwire.NumKindUID + } + if !dec.ExpectNumSet(numKind, &data.All) { + return "", nil, dec.Err() + } + if data.All.Dynamic() { + return "", nil, fmt.Errorf("imapclient: server returned a dynamic ALL number set in SEARCH response") + } + case "COUNT": + var num uint32 + if !dec.ExpectNumber(&num) { + return "", nil, dec.Err() + } + data.Count = num + case "MODSEQ": + var modSeq uint64 + if !dec.ExpectModSeq(&modSeq) { + return "", nil, dec.Err() + } + data.ModSeq = modSeq + default: + if !dec.DiscardValue() { + return "", nil, dec.Err() + } + } + + if !dec.SP() { + break + } else if !dec.ExpectAtom(&name) { + return "", nil, dec.Err() + } + } + + return tag, data, nil +} + +func searchCriteriaIsASCII(criteria *imap.SearchCriteria) bool { + for _, kv := range criteria.Header { + if !isASCII(kv.Key) || !isASCII(kv.Value) { + return false + } + } + for _, s := range criteria.Body { + if !isASCII(s) { + return false + } + } + for _, s := range criteria.Text { + if !isASCII(s) { + return false + } + } + for _, not := range criteria.Not { + if !searchCriteriaIsASCII(¬) { + return false + } + } + for _, or := range criteria.Or { + if !searchCriteriaIsASCII(&or[0]) || !searchCriteriaIsASCII(&or[1]) { + return false + } + } + return true +} + +func isASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] > unicode.MaxASCII { + return false + } + } + return true +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/select.go b/vendor/github.com/emersion/go-imap/v2/imapclient/select.go new file mode 100644 index 000000000..90cd11545 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/select.go @@ -0,0 +1,100 @@ +package imapclient + +import ( + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal" +) + +// Select sends a SELECT or EXAMINE command. +// +// A nil options pointer is equivalent to a zero options value. +func (c *Client) Select(mailbox string, options *imap.SelectOptions) *SelectCommand { + cmdName := "SELECT" + if options != nil && options.ReadOnly { + cmdName = "EXAMINE" + } + + cmd := &SelectCommand{mailbox: mailbox} + enc := c.beginCommand(cmdName, cmd) + enc.SP().Mailbox(mailbox) + if options != nil && options.CondStore { + enc.SP().Special('(').Atom("CONDSTORE").Special(')') + } + enc.end() + return cmd +} + +// Unselect sends an UNSELECT command. +// +// This command requires support for IMAP4rev2 or the UNSELECT extension. +func (c *Client) Unselect() *Command { + cmd := &unselectCommand{} + c.beginCommand("UNSELECT", cmd).end() + return &cmd.Command +} + +// UnselectAndExpunge sends a CLOSE command. +// +// CLOSE implicitly performs a silent EXPUNGE command. +func (c *Client) UnselectAndExpunge() *Command { + cmd := &unselectCommand{} + c.beginCommand("CLOSE", cmd).end() + return &cmd.Command +} + +func (c *Client) handleFlags() error { + flags, err := internal.ExpectFlagList(c.dec) + if err != nil { + return err + } + + c.mutex.Lock() + if c.state == imap.ConnStateSelected { + c.mailbox = c.mailbox.copy() + c.mailbox.PermanentFlags = flags + } + c.mutex.Unlock() + + cmd := findPendingCmdByType[*SelectCommand](c) + if cmd != nil { + cmd.data.Flags = flags + } else if handler := c.options.unilateralDataHandler().Mailbox; handler != nil { + handler(&UnilateralDataMailbox{Flags: flags}) + } + + return nil +} + +func (c *Client) handleExists(num uint32) error { + cmd := findPendingCmdByType[*SelectCommand](c) + if cmd != nil { + cmd.data.NumMessages = num + } else { + c.mutex.Lock() + if c.state == imap.ConnStateSelected { + c.mailbox = c.mailbox.copy() + c.mailbox.NumMessages = num + } + c.mutex.Unlock() + + if handler := c.options.unilateralDataHandler().Mailbox; handler != nil { + handler(&UnilateralDataMailbox{NumMessages: &num}) + } + } + return nil +} + +// SelectCommand is a SELECT command. +type SelectCommand struct { + commandBase + mailbox string + data imap.SelectData +} + +func (cmd *SelectCommand) Wait() (*imap.SelectData, error) { + return &cmd.data, cmd.wait() +} + +type unselectCommand struct { + Command +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/sort.go b/vendor/github.com/emersion/go-imap/v2/imapclient/sort.go new file mode 100644 index 000000000..260706d39 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/sort.go @@ -0,0 +1,84 @@ +package imapclient + +import ( + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +type SortKey string + +const ( + SortKeyArrival SortKey = "ARRIVAL" + SortKeyCc SortKey = "CC" + SortKeyDate SortKey = "DATE" + SortKeyFrom SortKey = "FROM" + SortKeySize SortKey = "SIZE" + SortKeySubject SortKey = "SUBJECT" + SortKeyTo SortKey = "TO" +) + +type SortCriterion struct { + Key SortKey + Reverse bool +} + +// SortOptions contains options for the SORT command. +type SortOptions struct { + SearchCriteria *imap.SearchCriteria + SortCriteria []SortCriterion +} + +func (c *Client) sort(numKind imapwire.NumKind, options *SortOptions) *SortCommand { + cmd := &SortCommand{} + enc := c.beginCommand(uidCmdName("SORT", numKind), cmd) + enc.SP().List(len(options.SortCriteria), func(i int) { + criterion := options.SortCriteria[i] + if criterion.Reverse { + enc.Atom("REVERSE").SP() + } + enc.Atom(string(criterion.Key)) + }) + enc.SP().Atom("UTF-8").SP() + writeSearchKey(enc.Encoder, options.SearchCriteria) + enc.end() + return cmd +} + +func (c *Client) handleSort() error { + cmd := findPendingCmdByType[*SortCommand](c) + for c.dec.SP() { + var num uint32 + if !c.dec.ExpectNumber(&num) { + return c.dec.Err() + } + if cmd != nil { + cmd.nums = append(cmd.nums, num) + } + } + return nil +} + +// Sort sends a SORT command. +// +// This command requires support for the SORT extension. +func (c *Client) Sort(options *SortOptions) *SortCommand { + return c.sort(imapwire.NumKindSeq, options) +} + +// UIDSort sends a UID SORT command. +// +// See Sort. +func (c *Client) UIDSort(options *SortOptions) *SortCommand { + return c.sort(imapwire.NumKindUID, options) +} + +// SortCommand is a SORT command. +type SortCommand struct { + commandBase + nums []uint32 +} + +func (cmd *SortCommand) Wait() ([]uint32, error) { + err := cmd.wait() + return cmd.nums, err +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/starttls.go b/vendor/github.com/emersion/go-imap/v2/imapclient/starttls.go new file mode 100644 index 000000000..8b63cca14 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/starttls.go @@ -0,0 +1,83 @@ +package imapclient + +import ( + "bufio" + "bytes" + "crypto/tls" + "io" + "net" +) + +// startTLS sends a STARTTLS command. +// +// Unlike other commands, this method blocks until the command completes. +func (c *Client) startTLS(config *tls.Config) error { + upgradeDone := make(chan struct{}) + cmd := &startTLSCommand{ + tlsConfig: config, + upgradeDone: upgradeDone, + } + enc := c.beginCommand("STARTTLS", cmd) + enc.flush() + defer enc.end() + + // Once a client issues a STARTTLS command, it MUST NOT issue further + // commands until a server response is seen and the TLS negotiation is + // complete + + if err := cmd.wait(); err != nil { + return err + } + + // The decoder goroutine will invoke Client.upgradeStartTLS + <-upgradeDone + + return cmd.tlsConn.Handshake() +} + +// upgradeStartTLS finishes the STARTTLS upgrade after the server has sent an +// OK response. It runs in the decoder goroutine. +func (c *Client) upgradeStartTLS(startTLS *startTLSCommand) { + defer close(startTLS.upgradeDone) + + // Drain buffered data from our bufio.Reader + var buf bytes.Buffer + if _, err := io.CopyN(&buf, c.br, int64(c.br.Buffered())); err != nil { + panic(err) // unreachable + } + + var cleartextConn net.Conn + if buf.Len() > 0 { + r := io.MultiReader(&buf, c.conn) + cleartextConn = startTLSConn{c.conn, r} + } else { + cleartextConn = c.conn + } + + tlsConn := tls.Client(cleartextConn, startTLS.tlsConfig) + rw := c.options.wrapReadWriter(tlsConn) + + c.br.Reset(rw) + // Unfortunately we can't re-use the bufio.Writer here, it races with + // Client.StartTLS + c.bw = bufio.NewWriter(rw) + + startTLS.tlsConn = tlsConn +} + +type startTLSCommand struct { + commandBase + tlsConfig *tls.Config + + upgradeDone chan<- struct{} + tlsConn *tls.Conn +} + +type startTLSConn struct { + net.Conn + r io.Reader +} + +func (conn startTLSConn) Read(b []byte) (int, error) { + return conn.r.Read(b) +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/status.go b/vendor/github.com/emersion/go-imap/v2/imapclient/status.go new file mode 100644 index 000000000..86cf0c331 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/status.go @@ -0,0 +1,161 @@ +package imapclient + +import ( + "fmt" + "strings" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +func statusItems(options *imap.StatusOptions) []string { + m := map[string]bool{ + "MESSAGES": options.NumMessages, + "UIDNEXT": options.UIDNext, + "UIDVALIDITY": options.UIDValidity, + "UNSEEN": options.NumUnseen, + "DELETED": options.NumDeleted, + "SIZE": options.Size, + "APPENDLIMIT": options.AppendLimit, + "DELETED-STORAGE": options.DeletedStorage, + "HIGHESTMODSEQ": options.HighestModSeq, + } + + var l []string + for k, req := range m { + if req { + l = append(l, k) + } + } + return l +} + +// Status sends a STATUS command. +// +// A nil options pointer is equivalent to a zero options value. +func (c *Client) Status(mailbox string, options *imap.StatusOptions) *StatusCommand { + if options == nil { + options = new(imap.StatusOptions) + } + + cmd := &StatusCommand{mailbox: mailbox} + enc := c.beginCommand("STATUS", cmd) + enc.SP().Mailbox(mailbox).SP() + items := statusItems(options) + enc.List(len(items), func(i int) { + enc.Atom(items[i]) + }) + enc.end() + return cmd +} + +func (c *Client) handleStatus() error { + data, err := readStatus(c.dec) + if err != nil { + return fmt.Errorf("in status: %v", err) + } + + cmd := c.findPendingCmdFunc(func(cmd command) bool { + switch cmd := cmd.(type) { + case *StatusCommand: + return cmd.mailbox == data.Mailbox + case *ListCommand: + return cmd.returnStatus && cmd.pendingData != nil && cmd.pendingData.Mailbox == data.Mailbox + default: + return false + } + }) + switch cmd := cmd.(type) { + case *StatusCommand: + cmd.data = *data + case *ListCommand: + cmd.pendingData.Status = data + cmd.mailboxes <- cmd.pendingData + cmd.pendingData = nil + } + + return nil +} + +// StatusCommand is a STATUS command. +type StatusCommand struct { + commandBase + mailbox string + data imap.StatusData +} + +func (cmd *StatusCommand) Wait() (*imap.StatusData, error) { + return &cmd.data, cmd.wait() +} + +func readStatus(dec *imapwire.Decoder) (*imap.StatusData, error) { + var data imap.StatusData + + if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() { + return nil, dec.Err() + } + + err := dec.ExpectList(func() error { + if err := readStatusAttVal(dec, &data); err != nil { + return fmt.Errorf("in status-att-val: %v", dec.Err()) + } + return nil + }) + return &data, err +} + +func readStatusAttVal(dec *imapwire.Decoder, data *imap.StatusData) error { + var name string + if !dec.ExpectAtom(&name) || !dec.ExpectSP() { + return dec.Err() + } + + var ok bool + switch strings.ToUpper(name) { + case "MESSAGES": + var num uint32 + ok = dec.ExpectNumber(&num) + data.NumMessages = &num + case "UIDNEXT": + var uidNext imap.UID + ok = dec.ExpectUID(&uidNext) + data.UIDNext = uidNext + case "UIDVALIDITY": + ok = dec.ExpectNumber(&data.UIDValidity) + case "UNSEEN": + var num uint32 + ok = dec.ExpectNumber(&num) + data.NumUnseen = &num + case "DELETED": + var num uint32 + ok = dec.ExpectNumber(&num) + data.NumDeleted = &num + case "SIZE": + var size int64 + ok = dec.ExpectNumber64(&size) + data.Size = &size + case "APPENDLIMIT": + var num uint32 + if dec.Number(&num) { + ok = true + } else { + ok = dec.ExpectNIL() + num = ^uint32(0) + } + data.AppendLimit = &num + case "DELETED-STORAGE": + var storage int64 + ok = dec.ExpectNumber64(&storage) + data.DeletedStorage = &storage + case "HIGHESTMODSEQ": + ok = dec.ExpectModSeq(&data.HighestModSeq) + default: + if !dec.DiscardValue() { + return dec.Err() + } + } + if !ok { + return dec.Err() + } + return nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/store.go b/vendor/github.com/emersion/go-imap/v2/imapclient/store.go new file mode 100644 index 000000000..a8be6d107 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/store.go @@ -0,0 +1,44 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// Store sends a STORE command. +// +// Unless StoreFlags.Silent is set, the server will return the updated values. +// +// A nil options pointer is equivalent to a zero options value. +func (c *Client) Store(numSet imap.NumSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand { + cmd := &FetchCommand{ + numSet: numSet, + msgs: make(chan *FetchMessageData, 128), + } + enc := c.beginCommand(uidCmdName("STORE", imapwire.NumSetKind(numSet)), cmd) + enc.SP().NumSet(numSet).SP() + if options != nil && options.UnchangedSince != 0 { + enc.Special('(').Atom("UNCHANGEDSINCE").SP().ModSeq(options.UnchangedSince).Special(')').SP() + } + switch store.Op { + case imap.StoreFlagsSet: + // nothing to do + case imap.StoreFlagsAdd: + enc.Special('+') + case imap.StoreFlagsDel: + enc.Special('-') + default: + panic(fmt.Errorf("imapclient: unknown store flags op: %v", store.Op)) + } + enc.Atom("FLAGS") + if store.Silent { + enc.Atom(".SILENT") + } + enc.SP().List(len(store.Flags), func(i int) { + enc.Flag(store.Flags[i]) + }) + enc.end() + return cmd +} diff --git a/vendor/github.com/emersion/go-imap/v2/imapclient/thread.go b/vendor/github.com/emersion/go-imap/v2/imapclient/thread.go new file mode 100644 index 000000000..c341a18ef --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/imapclient/thread.go @@ -0,0 +1,85 @@ +package imapclient + +import ( + "fmt" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +// ThreadOptions contains options for the THREAD command. +type ThreadOptions struct { + Algorithm imap.ThreadAlgorithm + SearchCriteria *imap.SearchCriteria +} + +func (c *Client) thread(numKind imapwire.NumKind, options *ThreadOptions) *ThreadCommand { + cmd := &ThreadCommand{} + enc := c.beginCommand(uidCmdName("THREAD", numKind), cmd) + enc.SP().Atom(string(options.Algorithm)).SP().Atom("UTF-8").SP() + writeSearchKey(enc.Encoder, options.SearchCriteria) + enc.end() + return cmd +} + +// Thread sends a THREAD command. +// +// This command requires support for the THREAD extension. +func (c *Client) Thread(options *ThreadOptions) *ThreadCommand { + return c.thread(imapwire.NumKindSeq, options) +} + +// UIDThread sends a UID THREAD command. +// +// See Thread. +func (c *Client) UIDThread(options *ThreadOptions) *ThreadCommand { + return c.thread(imapwire.NumKindUID, options) +} + +func (c *Client) handleThread() error { + cmd := findPendingCmdByType[*ThreadCommand](c) + for c.dec.SP() { + data, err := readThreadList(c.dec) + if err != nil { + return fmt.Errorf("in thread-list: %v", err) + } + if cmd != nil { + cmd.data = append(cmd.data, *data) + } + } + return nil +} + +// ThreadCommand is a THREAD command. +type ThreadCommand struct { + commandBase + data []ThreadData +} + +func (cmd *ThreadCommand) Wait() ([]ThreadData, error) { + err := cmd.wait() + return cmd.data, err +} + +type ThreadData struct { + Chain []uint32 + SubThreads []ThreadData +} + +func readThreadList(dec *imapwire.Decoder) (*ThreadData, error) { + var data ThreadData + err := dec.ExpectList(func() error { + var num uint32 + if len(data.SubThreads) == 0 && dec.Number(&num) { + data.Chain = append(data.Chain, num) + } else { + sub, err := readThreadList(dec) + if err != nil { + return err + } + data.SubThreads = append(data.SubThreads, *sub) + } + return nil + }) + return &data, err +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/acl.go b/vendor/github.com/emersion/go-imap/v2/internal/acl.go new file mode 100644 index 000000000..43c078751 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/acl.go @@ -0,0 +1,13 @@ +package internal + +import ( + "github.com/emersion/go-imap/v2" +) + +func FormatRights(rm imap.RightModification, rs imap.RightSet) string { + s := "" + if rm != imap.RightModificationReplace { + s = string(rm) + } + return s + string(rs) +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/imapnum/numset.go b/vendor/github.com/emersion/go-imap/v2/internal/imapnum/numset.go new file mode 100644 index 000000000..25a4f292f --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/imapnum/numset.go @@ -0,0 +1,306 @@ +package imapnum + +import ( + "fmt" + "strconv" + "strings" +) + +// Range represents a single seq-number or seq-range value (RFC 3501 ABNF). Values +// may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is +// represented by setting Start = Stop. Zero is used to represent "*", which is +// safe because seq-number uses nz-number rule. The order of values is always +// Start <= Stop, except when representing "n:*", where Start = n and Stop = 0. +type Range struct { + Start, Stop uint32 +} + +// Contains returns true if the seq-number q is contained in range value s. +// The dynamic value "*" contains only other "*" values, the dynamic range "n:*" +// contains "*" and all numbers >= n. +func (s Range) Contains(q uint32) bool { + if q == 0 { + return s.Stop == 0 // "*" is contained only in "*" and "n:*" + } + return s.Start != 0 && s.Start <= q && (q <= s.Stop || s.Stop == 0) +} + +// Less returns true if s precedes and does not contain seq-number q. +func (s Range) Less(q uint32) bool { + return (s.Stop < q || q == 0) && s.Stop != 0 +} + +// Merge combines range values s and t into a single union if the two +// intersect or one is a superset of the other. The order of s and t does not +// matter. If the values cannot be merged, s is returned unmodified and ok is +// set to false. +func (s Range) Merge(t Range) (union Range, ok bool) { + union = s + if s == t { + return s, true + } + if s.Start != 0 && t.Start != 0 { + // s and t are any combination of "n", "n:m", or "n:*" + if s.Start > t.Start { + s, t = t, s + } + // s starts at or before t, check where it ends + if (s.Stop >= t.Stop && t.Stop != 0) || s.Stop == 0 { + return s, true // s is a superset of t + } + // s is "n" or "n:m", if m == ^uint32(0) then t is "n:*" + if s.Stop+1 >= t.Start || s.Stop == ^uint32(0) { + return Range{s.Start, t.Stop}, true // s intersects or touches t + } + return union, false + } + // exactly one of s and t is "*" + if s.Start == 0 { + if t.Stop == 0 { + return t, true // s is "*", t is "n:*" + } + } else if s.Stop == 0 { + return s, true // s is "n:*", t is "*" + } + return union, false +} + +// String returns range value s as a seq-number or seq-range string. +func (s Range) String() string { + if s.Start == s.Stop { + if s.Start == 0 { + return "*" + } + return strconv.FormatUint(uint64(s.Start), 10) + } + b := strconv.AppendUint(make([]byte, 0, 24), uint64(s.Start), 10) + if s.Stop == 0 { + return string(append(b, ':', '*')) + } + return string(strconv.AppendUint(append(b, ':'), uint64(s.Stop), 10)) +} + +func (s Range) append(nums []uint32) (out []uint32, ok bool) { + if s.Start == 0 || s.Stop == 0 { + return nil, false + } + for n := s.Start; n <= s.Stop; n++ { + nums = append(nums, n) + } + return nums, true +} + +// Set is used to represent a set of message sequence numbers or UIDs (see +// sequence-set ABNF rule). The zero value is an empty set. +type Set []Range + +// AddNum inserts new numbers into the set. The value 0 represents "*". +func (s *Set) AddNum(q ...uint32) { + for _, v := range q { + s.insert(Range{v, v}) + } +} + +// AddRange inserts a new range into the set. +func (s *Set) AddRange(start, stop uint32) { + if (stop < start && stop != 0) || start == 0 { + s.insert(Range{stop, start}) + } else { + s.insert(Range{start, stop}) + } +} + +// AddSet inserts all values from t into s. +func (s *Set) AddSet(t Set) { + for _, v := range t { + s.insert(v) + } +} + +// Dynamic returns true if the set contains "*" or "n:*" values. +func (s Set) Dynamic() bool { + return len(s) > 0 && s[len(s)-1].Stop == 0 +} + +// Contains returns true if the non-zero sequence number or UID q is contained +// in the set. The dynamic range "n:*" contains all q >= n. It is the caller's +// responsibility to handle the special case where q is the maximum UID in the +// mailbox and q < n (i.e. the set cannot match UIDs against "*:n" or "*" since +// it doesn't know what the maximum value is). +func (s Set) Contains(q uint32) bool { + if _, ok := s.search(q); ok { + return q != 0 + } + return false +} + +// Nums returns a slice of all numbers contained in the set. +func (s Set) Nums() (nums []uint32, ok bool) { + for _, v := range s { + nums, ok = v.append(nums) + if !ok { + return nil, false + } + } + return nums, true +} + +// String returns a sorted representation of all contained number values. +func (s Set) String() string { + if len(s) == 0 { + return "" + } + b := make([]byte, 0, 64) + for _, v := range s { + b = append(b, ',') + if v.Start == 0 { + b = append(b, '*') + continue + } + b = strconv.AppendUint(b, uint64(v.Start), 10) + if v.Start != v.Stop { + if v.Stop == 0 { + b = append(b, ':', '*') + continue + } + b = strconv.AppendUint(append(b, ':'), uint64(v.Stop), 10) + } + } + return string(b[1:]) +} + +// insert adds range value v to the set. +func (ptr *Set) insert(v Range) { + s := *ptr + defer func() { + *ptr = s + }() + + i, _ := s.search(v.Start) + merged := false + if i > 0 { + // try merging with the preceding entry (e.g. "1,4".insert(2), i == 1) + s[i-1], merged = s[i-1].Merge(v) + } + if i == len(s) { + // v was either merged with the last entry or needs to be appended + if !merged { + s.insertAt(i, v) + } + return + } else if merged { + i-- + } else if s[i], merged = s[i].Merge(v); !merged { + s.insertAt(i, v) // insert in the middle (e.g. "1,5".insert(3), i == 1) + return + } + // v was merged with s[i], continue trying to merge until the end + for j := i + 1; j < len(s); j++ { + if s[i], merged = s[i].Merge(s[j]); !merged { + if j > i+1 { + // cut out all entries between i and j that were merged + s = append(s[:i+1], s[j:]...) + } + return + } + } + // everything after s[i] was merged + s = s[:i+1] +} + +// insertAt inserts a new range value v at index i, resizing s.Set as needed. +func (ptr *Set) insertAt(i int, v Range) { + s := *ptr + defer func() { + *ptr = s + }() + + if n := len(s); i == n { + // insert at the end + s = append(s, v) + return + } else if n < cap(s) { + // enough space, shift everything at and after i to the right + s = s[:n+1] + copy(s[i+1:], s[i:]) + } else { + // allocate new slice and copy everything, n is at least 1 + set := make([]Range, n+1, n*2) + copy(set, s[:i]) + copy(set[i+1:], s[i:]) + s = set + } + s[i] = v +} + +// search attempts to find the index of the range set value that contains q. +// If no values contain q, the returned index is the position where q should be +// inserted and ok is set to false. +func (s Set) search(q uint32) (i int, ok bool) { + min, max := 0, len(s)-1 + for min < max { + if mid := (min + max) >> 1; s[mid].Less(q) { + min = mid + 1 + } else { + max = mid + } + } + if max < 0 || s[min].Less(q) { + return len(s), false // q is the new largest value + } + return min, s[min].Contains(q) +} + +// errBadNumSet is used to report problems with the format of a number set +// value. +type errBadNumSet string + +func (err errBadNumSet) Error() string { + return fmt.Sprintf("imap: bad number set value %q", string(err)) +} + +// parseNum parses a single seq-number value (non-zero uint32 or "*"). +func parseNum(v string) (uint32, error) { + if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' { + return uint32(n), nil + } else if v == "*" { + return 0, nil + } + return 0, errBadNumSet(v) +} + +// parseNumRange creates a new seq instance by parsing strings in the format +// "n" or "n:m", where n and/or m may be "*". An error is returned for invalid +// values. +func parseNumRange(v string) (Range, error) { + var ( + r Range + err error + ) + if sep := strings.IndexRune(v, ':'); sep < 0 { + r.Start, err = parseNum(v) + r.Stop = r.Start + return r, err + } else if r.Start, err = parseNum(v[:sep]); err == nil { + if r.Stop, err = parseNum(v[sep+1:]); err == nil { + if (r.Stop < r.Start && r.Stop != 0) || r.Start == 0 { + r.Start, r.Stop = r.Stop, r.Start + } + return r, nil + } + } + return r, errBadNumSet(v) +} + +// ParseSet returns a new Set after parsing the set string. +func ParseSet(set string) (Set, error) { + var s Set + for _, sv := range strings.Split(set, ",") { + r, err := parseNumRange(sv) + if err != nil { + return s, err + } + s.AddRange(r.Start, r.Stop) + } + return s, nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/imapwire/decoder.go b/vendor/github.com/emersion/go-imap/v2/internal/imapwire/decoder.go new file mode 100644 index 000000000..cfd2995c0 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/imapwire/decoder.go @@ -0,0 +1,654 @@ +package imapwire + +import ( + "bufio" + "fmt" + "io" + "strconv" + "strings" + "unicode" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapnum" + "github.com/emersion/go-imap/v2/internal/utf7" +) + +// This limits the max list nesting depth to prevent stack overflow. +const maxListDepth = 1000 + +// IsAtomChar returns true if ch is an ATOM-CHAR. +func IsAtomChar(ch byte) bool { + switch ch { + case '(', ')', '{', ' ', '%', '*', '"', '\\', ']': + return false + default: + return !unicode.IsControl(rune(ch)) + } +} + +// Is non-empty char +func isAStringChar(ch byte) bool { + return IsAtomChar(ch) || ch == ']' +} + +// DecoderExpectError is an error due to the Decoder.Expect family of methods. +type DecoderExpectError struct { + Message string +} + +func (err *DecoderExpectError) Error() string { + return fmt.Sprintf("imapwire: %v", err.Message) +} + +// A Decoder reads IMAP data. +// +// There are multiple families of methods: +// +// - Methods directly named after IMAP grammar elements attempt to decode +// said element, and return false if it's another element. +// - "Expect" methods do the same, but set the decoder error (see Err) on +// failure. +type Decoder struct { + // CheckBufferedLiteralFunc is called when a literal is about to be decoded + // and needs to be fully buffered in memory. + CheckBufferedLiteralFunc func(size int64, nonSync bool) error + // MaxSize defines a maximum number of bytes to be read from the input. + // Literals are ignored. + MaxSize int64 + + r *bufio.Reader + side ConnSide + err error + literal bool + crlf bool + listDepth int + readBytes int64 +} + +// NewDecoder creates a new decoder. +func NewDecoder(r *bufio.Reader, side ConnSide) *Decoder { + return &Decoder{r: r, side: side} +} + +func (dec *Decoder) mustUnreadByte() { + if err := dec.r.UnreadByte(); err != nil { + panic(fmt.Errorf("imapwire: failed to unread byte: %v", err)) + } + dec.readBytes-- +} + +// Err returns the decoder error, if any. +func (dec *Decoder) Err() error { + return dec.err +} + +func (dec *Decoder) returnErr(err error) bool { + if err == nil { + return true + } + if dec.err == nil { + dec.err = err + } + return false +} + +func (dec *Decoder) readByte() (byte, bool) { + if dec.MaxSize > 0 && dec.readBytes > dec.MaxSize { + return 0, dec.returnErr(fmt.Errorf("imapwire: max size exceeded")) + } + dec.crlf = false + if dec.literal { + return 0, dec.returnErr(fmt.Errorf("imapwire: cannot decode while a literal is open")) + } + b, err := dec.r.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return b, dec.returnErr(err) + } + dec.readBytes++ + return b, true +} + +func (dec *Decoder) acceptByte(want byte) bool { + got, ok := dec.readByte() + if !ok { + return false + } else if got != want { + dec.mustUnreadByte() + return false + } + return true +} + +// EOF returns true if end-of-file is reached. +func (dec *Decoder) EOF() bool { + _, err := dec.r.ReadByte() + if err == io.EOF { + return true + } else if err != nil { + return dec.returnErr(err) + } + dec.mustUnreadByte() + return false +} + +// Expect sets the decoder error if ok is false. +func (dec *Decoder) Expect(ok bool, name string) bool { + if !ok { + msg := fmt.Sprintf("expected %v", name) + if dec.r.Buffered() > 0 { + b, _ := dec.r.Peek(1) + msg += fmt.Sprintf(", got %q", b) + } + return dec.returnErr(&DecoderExpectError{Message: msg}) + } + return true +} + +func (dec *Decoder) SP() bool { + if dec.acceptByte(' ') { + // https://github.com/emersion/go-imap/issues/571 + b, ok := dec.readByte() + if !ok { + return false + } + dec.mustUnreadByte() + return b != '\r' && b != '\n' + } + + // Special case: SP is optional if the next field is a parenthesized list + b, ok := dec.readByte() + if !ok { + return false + } + dec.mustUnreadByte() + return b == '(' +} + +func (dec *Decoder) ExpectSP() bool { + return dec.Expect(dec.SP(), "SP") +} + +func (dec *Decoder) CRLF() bool { + dec.acceptByte(' ') // https://github.com/emersion/go-imap/issues/540 + dec.acceptByte('\r') // be liberal in what we receive and accept lone LF + if !dec.acceptByte('\n') { + return false + } + dec.crlf = true + return true +} + +func (dec *Decoder) ExpectCRLF() bool { + return dec.Expect(dec.CRLF(), "CRLF") +} + +func (dec *Decoder) Func(ptr *string, valid func(ch byte) bool) bool { + var sb strings.Builder + for { + b, ok := dec.readByte() + if !ok { + return false + } + + if !valid(b) { + dec.mustUnreadByte() + break + } + + sb.WriteByte(b) + } + if sb.Len() == 0 { + return false + } + *ptr = sb.String() + return true +} + +func (dec *Decoder) Atom(ptr *string) bool { + return dec.Func(ptr, IsAtomChar) +} + +func (dec *Decoder) ExpectAtom(ptr *string) bool { + return dec.Expect(dec.Atom(ptr), "atom") +} + +func (dec *Decoder) ExpectNIL() bool { + var s string + return dec.ExpectAtom(&s) && dec.Expect(s == "NIL", "NIL") +} + +func (dec *Decoder) Special(b byte) bool { + return dec.acceptByte(b) +} + +func (dec *Decoder) ExpectSpecial(b byte) bool { + return dec.Expect(dec.Special(b), fmt.Sprintf("'%v'", string(b))) +} + +func (dec *Decoder) Text(ptr *string) bool { + var sb strings.Builder + for { + b, ok := dec.readByte() + if !ok { + return false + } else if b == '\r' || b == '\n' { + dec.mustUnreadByte() + break + } + sb.WriteByte(b) + } + if sb.Len() == 0 { + return false + } + *ptr = sb.String() + return true +} + +func (dec *Decoder) ExpectText(ptr *string) bool { + return dec.Expect(dec.Text(ptr), "text") +} + +func (dec *Decoder) DiscardUntilByte(untilCh byte) { + for { + ch, ok := dec.readByte() + if !ok { + return + } else if ch == untilCh { + dec.mustUnreadByte() + return + } + } +} + +func (dec *Decoder) DiscardLine() { + if dec.crlf { + return + } + var text string + dec.Text(&text) + dec.CRLF() +} + +func (dec *Decoder) DiscardValue() bool { + var s string + if dec.String(&s) { + return true + } + + isList, err := dec.List(func() error { + if !dec.DiscardValue() { + return dec.Err() + } + return nil + }) + if err != nil { + return false + } else if isList { + return true + } + + if dec.Atom(&s) { + return true + } + + dec.Expect(false, "value") + return false +} + +func (dec *Decoder) numberStr() (s string, ok bool) { + var sb strings.Builder + for { + ch, ok := dec.readByte() + if !ok { + return "", false + } else if ch < '0' || ch > '9' { + dec.mustUnreadByte() + break + } + sb.WriteByte(ch) + } + if sb.Len() == 0 { + return "", false + } + return sb.String(), true +} + +func (dec *Decoder) Number(ptr *uint32) bool { + s, ok := dec.numberStr() + if !ok { + return false + } + v64, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return false // can happen on overflow + } + *ptr = uint32(v64) + return true +} + +func (dec *Decoder) ExpectNumber(ptr *uint32) bool { + return dec.Expect(dec.Number(ptr), "number") +} + +func (dec *Decoder) ExpectBodyFldOctets(ptr *uint32) bool { + // Workaround: some servers incorrectly return "-1" for the body structure + // size. See: + // https://github.com/emersion/go-imap/issues/534 + if dec.acceptByte('-') { + *ptr = 0 + return dec.Expect(dec.acceptByte('1'), "-1 (body-fld-octets workaround)") + } + return dec.ExpectNumber(ptr) +} + +func (dec *Decoder) Number64(ptr *int64) bool { + s, ok := dec.numberStr() + if !ok { + return false + } + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return false // can happen on overflow + } + *ptr = v + return true +} + +func (dec *Decoder) ExpectNumber64(ptr *int64) bool { + return dec.Expect(dec.Number64(ptr), "number64") +} + +func (dec *Decoder) ModSeq(ptr *uint64) bool { + s, ok := dec.numberStr() + if !ok { + return false + } + v, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return false // can happen on overflow + } + *ptr = v + return true +} + +func (dec *Decoder) ExpectModSeq(ptr *uint64) bool { + return dec.Expect(dec.ModSeq(ptr), "mod-sequence-value") +} + +func (dec *Decoder) Quoted(ptr *string) bool { + if !dec.Special('"') { + return false + } + var sb strings.Builder + for { + ch, ok := dec.readByte() + if !ok { + return false + } + + if ch == '"' { + break + } + + if ch == '\\' { + ch, ok = dec.readByte() + if !ok { + return false + } + } + + sb.WriteByte(ch) + } + *ptr = sb.String() + return true +} + +func (dec *Decoder) ExpectAString(ptr *string) bool { + if dec.Quoted(ptr) { + return true + } + if dec.Literal(ptr) { + return true + } + // We cannot do dec.Atom(ptr) here because sometimes mailbox names are unquoted, + // and they can contain special characters like `]`. + return dec.Expect(dec.Func(ptr, isAStringChar), "ASTRING-CHAR") +} + +func (dec *Decoder) String(ptr *string) bool { + return dec.Quoted(ptr) || dec.Literal(ptr) +} + +func (dec *Decoder) ExpectString(ptr *string) bool { + return dec.Expect(dec.String(ptr), "string") +} + +func (dec *Decoder) ExpectNString(ptr *string) bool { + var s string + if dec.Atom(&s) { + if !dec.Expect(s == "NIL", "nstring") { + return false + } + *ptr = "" + return true + } + return dec.ExpectString(ptr) +} + +func (dec *Decoder) ExpectNStringReader() (lit *LiteralReader, nonSync, ok bool) { + var s string + if dec.Atom(&s) { + if !dec.Expect(s == "NIL", "nstring") { + return nil, false, false + } + return nil, true, true + } + // TODO: read quoted string as a string instead of buffering + if dec.Quoted(&s) { + return newLiteralReaderFromString(s), true, true + } + if lit, nonSync, ok = dec.LiteralReader(); ok { + return lit, nonSync, true + } else { + return nil, false, dec.Expect(false, "nstring") + } +} + +func (dec *Decoder) List(f func() error) (isList bool, err error) { + if !dec.Special('(') { + return false, nil + } + if dec.Special(')') { + return true, nil + } + + dec.listDepth++ + defer func() { + dec.listDepth-- + }() + + if dec.listDepth >= maxListDepth { + return false, fmt.Errorf("imapwire: exceeded max depth") + } + + for { + if err := f(); err != nil { + return true, err + } + + if dec.Special(')') { + return true, nil + } else if !dec.ExpectSP() { + return true, dec.Err() + } + } +} + +func (dec *Decoder) ExpectList(f func() error) error { + isList, err := dec.List(f) + if err != nil { + return err + } else if !dec.Expect(isList, "(") { + return dec.Err() + } + return nil +} + +func (dec *Decoder) ExpectNList(f func() error) error { + var s string + if dec.Atom(&s) { + if !dec.Expect(s == "NIL", "NIL") { + return dec.Err() + } + return nil + } + return dec.ExpectList(f) +} + +func (dec *Decoder) ExpectMailbox(ptr *string) bool { + var name string + if !dec.ExpectAString(&name) { + return false + } + if strings.EqualFold(name, "INBOX") { + *ptr = "INBOX" + return true + } + name, err := utf7.Decode(name) + if err == nil { + *ptr = name + } + return dec.returnErr(err) +} + +func (dec *Decoder) ExpectUID(ptr *imap.UID) bool { + var num uint32 + if !dec.ExpectNumber(&num) { + return false + } + *ptr = imap.UID(num) + return true +} + +func (dec *Decoder) ExpectNumSet(kind NumKind, ptr *imap.NumSet) bool { + if dec.Special('$') { + *ptr = imap.SearchRes() + return true + } + + var s string + if !dec.Expect(dec.Func(&s, isNumSetChar), "sequence-set") { + return false + } + numSet, err := imapnum.ParseSet(s) + if err != nil { + return dec.returnErr(err) + } + + switch kind { + case NumKindSeq: + *ptr = seqSetFromNumSet(numSet) + case NumKindUID: + *ptr = uidSetFromNumSet(numSet) + } + return true +} + +func (dec *Decoder) ExpectUIDSet(ptr *imap.UIDSet) bool { + var numSet imap.NumSet + ok := dec.ExpectNumSet(NumKindUID, &numSet) + if ok { + *ptr = numSet.(imap.UIDSet) + } + return ok +} + +func isNumSetChar(ch byte) bool { + return ch == '*' || IsAtomChar(ch) +} + +func (dec *Decoder) Literal(ptr *string) bool { + lit, nonSync, ok := dec.LiteralReader() + if !ok { + return false + } + if dec.CheckBufferedLiteralFunc != nil { + if err := dec.CheckBufferedLiteralFunc(lit.Size(), nonSync); err != nil { + lit.cancel() + return false + } + } + var sb strings.Builder + _, err := io.Copy(&sb, lit) + if err == nil { + *ptr = sb.String() + } + return dec.returnErr(err) +} + +func (dec *Decoder) LiteralReader() (lit *LiteralReader, nonSync, ok bool) { + if !dec.Special('{') { + return nil, false, false + } + var size int64 + if !dec.ExpectNumber64(&size) { + return nil, false, false + } + if dec.side == ConnSideServer { + nonSync = dec.acceptByte('+') + } + if !dec.ExpectSpecial('}') || !dec.ExpectCRLF() { + return nil, false, false + } + dec.literal = true + lit = &LiteralReader{ + dec: dec, + size: size, + r: io.LimitReader(dec.r, size), + } + return lit, nonSync, true +} + +func (dec *Decoder) ExpectLiteralReader() (lit *LiteralReader, nonSync bool, err error) { + lit, nonSync, ok := dec.LiteralReader() + if !dec.Expect(ok, "literal") { + return nil, false, dec.Err() + } + return lit, nonSync, nil +} + +type LiteralReader struct { + dec *Decoder + size int64 + r io.Reader +} + +func newLiteralReaderFromString(s string) *LiteralReader { + return &LiteralReader{ + size: int64(len(s)), + r: strings.NewReader(s), + } +} + +func (lit *LiteralReader) Size() int64 { + return lit.size +} + +func (lit *LiteralReader) Read(b []byte) (int, error) { + n, err := lit.r.Read(b) + if err == io.EOF { + lit.cancel() + } + return n, err +} + +func (lit *LiteralReader) cancel() { + if lit.dec == nil { + return + } + lit.dec.literal = false + lit.dec = nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/imapwire/encoder.go b/vendor/github.com/emersion/go-imap/v2/internal/imapwire/encoder.go new file mode 100644 index 000000000..b27589aa6 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/imapwire/encoder.go @@ -0,0 +1,341 @@ +package imapwire + +import ( + "bufio" + "fmt" + "io" + "strconv" + "strings" + "unicode" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/utf7" +) + +// An Encoder writes IMAP data. +// +// Most methods don't return an error, instead they defer error handling until +// CRLF is called. These methods return the Encoder so that calls can be +// chained. +type Encoder struct { + // QuotedUTF8 allows raw UTF-8 in quoted strings. This requires IMAP4rev2 + // to be available, or UTF8=ACCEPT to be enabled. + QuotedUTF8 bool + // LiteralMinus enables non-synchronizing literals for short payloads. + // This requires IMAP4rev2 or LITERAL-. This is only meaningful for + // clients. + LiteralMinus bool + // LiteralPlus enables non-synchronizing literals for all payloads. This + // requires LITERAL+. This is only meaningful for clients. + LiteralPlus bool + // NewContinuationRequest creates a new continuation request. This is only + // meaningful for clients. + NewContinuationRequest func() *ContinuationRequest + + w *bufio.Writer + side ConnSide + err error + literal bool +} + +// NewEncoder creates a new encoder. +func NewEncoder(w *bufio.Writer, side ConnSide) *Encoder { + return &Encoder{w: w, side: side} +} + +func (enc *Encoder) setErr(err error) { + if enc.err == nil { + enc.err = err + } +} + +func (enc *Encoder) writeString(s string) *Encoder { + if enc.err != nil { + return enc + } + if enc.literal { + enc.err = fmt.Errorf("imapwire: cannot encode while a literal is open") + return enc + } + if _, err := enc.w.WriteString(s); err != nil { + enc.err = err + } + return enc +} + +// CRLF writes a "\r\n" sequence and flushes the buffered writer. +func (enc *Encoder) CRLF() error { + enc.writeString("\r\n") + if enc.err != nil { + return enc.err + } + return enc.w.Flush() +} + +func (enc *Encoder) Atom(s string) *Encoder { + return enc.writeString(s) +} + +func (enc *Encoder) SP() *Encoder { + return enc.writeString(" ") +} + +func (enc *Encoder) Special(ch byte) *Encoder { + return enc.writeString(string(ch)) +} + +func (enc *Encoder) Quoted(s string) *Encoder { + var sb strings.Builder + sb.Grow(2 + len(s)) + sb.WriteByte('"') + for i := 0; i < len(s); i++ { + ch := s[i] + if ch == '"' || ch == '\\' { + sb.WriteByte('\\') + } + sb.WriteByte(ch) + } + sb.WriteByte('"') + return enc.writeString(sb.String()) +} + +func (enc *Encoder) String(s string) *Encoder { + if !enc.validQuoted(s) { + enc.stringLiteral(s) + return enc + } + return enc.Quoted(s) +} + +func (enc *Encoder) validQuoted(s string) bool { + if len(s) > 4096 { + return false + } + + for i := 0; i < len(s); i++ { + ch := s[i] + + // NUL, CR and LF are never valid + switch ch { + case 0, '\r', '\n': + return false + } + + if !enc.QuotedUTF8 && ch > unicode.MaxASCII { + return false + } + } + return true +} + +func (enc *Encoder) stringLiteral(s string) { + var sync *ContinuationRequest + if enc.side == ConnSideClient && (!enc.LiteralMinus || len(s) > 4096) && !enc.LiteralPlus { + if enc.NewContinuationRequest != nil { + sync = enc.NewContinuationRequest() + } + if sync == nil { + enc.setErr(fmt.Errorf("imapwire: cannot send synchronizing literal")) + return + } + } + wc := enc.Literal(int64(len(s)), sync) + _, writeErr := io.WriteString(wc, s) + closeErr := wc.Close() + if writeErr != nil { + enc.setErr(writeErr) + } else if closeErr != nil { + enc.setErr(closeErr) + } +} + +func (enc *Encoder) Mailbox(name string) *Encoder { + if strings.EqualFold(name, "INBOX") { + return enc.Atom("INBOX") + } else { + if enc.QuotedUTF8 { + name = utf7.Escape(name) + } else { + name = utf7.Encode(name) + } + return enc.String(name) + } +} + +func (enc *Encoder) NumSet(numSet imap.NumSet) *Encoder { + s := numSet.String() + if s == "" { + enc.setErr(fmt.Errorf("imapwire: cannot encode empty sequence set")) + return enc + } + return enc.writeString(s) +} + +func (enc *Encoder) Flag(flag imap.Flag) *Encoder { + if flag != "\\*" && !isValidFlag(string(flag)) { + enc.setErr(fmt.Errorf("imapwire: invalid flag %q", flag)) + return enc + } + return enc.writeString(string(flag)) +} + +func (enc *Encoder) MailboxAttr(attr imap.MailboxAttr) *Encoder { + if !strings.HasPrefix(string(attr), "\\") || !isValidFlag(string(attr)) { + enc.setErr(fmt.Errorf("imapwire: invalid mailbox attribute %q", attr)) + return enc + } + return enc.writeString(string(attr)) +} + +// isValidFlag checks whether the provided string satisfies +// flag-keyword / flag-extension. +func isValidFlag(s string) bool { + for i := 0; i < len(s); i++ { + ch := s[i] + if ch == '\\' { + if i != 0 { + return false + } + } else { + if !IsAtomChar(ch) { + return false + } + } + } + return len(s) > 0 +} + +func (enc *Encoder) Number(v uint32) *Encoder { + return enc.writeString(strconv.FormatUint(uint64(v), 10)) +} + +func (enc *Encoder) Number64(v int64) *Encoder { + // TODO: disallow negative values + return enc.writeString(strconv.FormatInt(v, 10)) +} + +func (enc *Encoder) ModSeq(v uint64) *Encoder { + // TODO: disallow zero values + return enc.writeString(strconv.FormatUint(v, 10)) +} + +// List writes a parenthesized list. +func (enc *Encoder) List(n int, f func(i int)) *Encoder { + enc.Special('(') + for i := 0; i < n; i++ { + if i > 0 { + enc.SP() + } + f(i) + } + enc.Special(')') + return enc +} + +func (enc *Encoder) BeginList() *ListEncoder { + enc.Special('(') + return &ListEncoder{enc: enc} +} + +func (enc *Encoder) NIL() *Encoder { + return enc.Atom("NIL") +} + +func (enc *Encoder) Text(s string) *Encoder { + return enc.writeString(s) +} + +func (enc *Encoder) UID(uid imap.UID) *Encoder { + return enc.Number(uint32(uid)) +} + +// Literal writes a literal. +// +// The caller must write exactly size bytes to the returned writer. +// +// If sync is non-nil, the literal is synchronizing: the encoder will wait for +// nil to be sent to the channel before writing the literal data. If an error +// is sent to the channel, the literal will be cancelled. +func (enc *Encoder) Literal(size int64, sync *ContinuationRequest) io.WriteCloser { + if sync != nil && enc.side == ConnSideServer { + panic("imapwire: sync must be nil on a server-side Encoder.Literal") + } + + // TODO: literal8 + enc.writeString("{") + enc.Number64(size) + if sync == nil && enc.side == ConnSideClient { + enc.writeString("+") + } + enc.writeString("}") + + if sync == nil { + enc.writeString("\r\n") + } else { + if err := enc.CRLF(); err != nil { + return errorWriter{err} + } + if _, err := sync.Wait(); err != nil { + enc.setErr(err) + return errorWriter{err} + } + } + + enc.literal = true + return &literalWriter{ + enc: enc, + n: size, + } +} + +type errorWriter struct { + err error +} + +func (ew errorWriter) Write(b []byte) (int, error) { + return 0, ew.err +} + +func (ew errorWriter) Close() error { + return ew.err +} + +type literalWriter struct { + enc *Encoder + n int64 +} + +func (lw *literalWriter) Write(b []byte) (int, error) { + if lw.n-int64(len(b)) < 0 { + return 0, fmt.Errorf("wrote too many bytes in literal") + } + n, err := lw.enc.w.Write(b) + lw.n -= int64(n) + return n, err +} + +func (lw *literalWriter) Close() error { + lw.enc.literal = false + if lw.n != 0 { + return fmt.Errorf("wrote too few bytes in literal (%v remaining)", lw.n) + } + return nil +} + +type ListEncoder struct { + enc *Encoder + n int +} + +func (le *ListEncoder) Item() *Encoder { + if le.n > 0 { + le.enc.SP() + } + le.n++ + return le.enc +} + +func (le *ListEncoder) End() { + le.enc.Special(')') + le.enc = nil +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/imapwire/imapwire.go b/vendor/github.com/emersion/go-imap/v2/internal/imapwire/imapwire.go new file mode 100644 index 000000000..716d1c2d0 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/imapwire/imapwire.go @@ -0,0 +1,47 @@ +// Package imapwire implements the IMAP wire protocol. +// +// The IMAP wire protocol is defined in RFC 9051 section 4. +package imapwire + +import ( + "fmt" +) + +// ConnSide describes the side of a connection: client or server. +type ConnSide int + +const ( + ConnSideClient ConnSide = 1 + iota + ConnSideServer +) + +// ContinuationRequest is a continuation request. +// +// The sender must call either Done or Cancel. The receiver must call Wait. +type ContinuationRequest struct { + done chan struct{} + err error + text string +} + +func NewContinuationRequest() *ContinuationRequest { + return &ContinuationRequest{done: make(chan struct{})} +} + +func (cont *ContinuationRequest) Cancel(err error) { + if err == nil { + err = fmt.Errorf("imapwire: continuation request cancelled") + } + cont.err = err + close(cont.done) +} + +func (cont *ContinuationRequest) Done(text string) { + cont.text = text + close(cont.done) +} + +func (cont *ContinuationRequest) Wait() (string, error) { + <-cont.done + return cont.text, cont.err +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/imapwire/num.go b/vendor/github.com/emersion/go-imap/v2/internal/imapwire/num.go new file mode 100644 index 000000000..270afe1ec --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/imapwire/num.go @@ -0,0 +1,39 @@ +package imapwire + +import ( + "unsafe" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapnum" +) + +type NumKind int + +const ( + NumKindSeq NumKind = iota + 1 + NumKindUID +) + +func seqSetFromNumSet(s imapnum.Set) imap.SeqSet { + return *(*imap.SeqSet)(unsafe.Pointer(&s)) +} + +func uidSetFromNumSet(s imapnum.Set) imap.UIDSet { + return *(*imap.UIDSet)(unsafe.Pointer(&s)) +} + +func NumSetKind(numSet imap.NumSet) NumKind { + switch numSet.(type) { + case imap.SeqSet: + return NumKindSeq + case imap.UIDSet: + return NumKindUID + default: + panic("imap: invalid NumSet type") + } +} + +func ParseSeqSet(s string) (imap.SeqSet, error) { + numSet, err := imapnum.ParseSet(s) + return seqSetFromNumSet(numSet), err +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/internal.go b/vendor/github.com/emersion/go-imap/v2/internal/internal.go new file mode 100644 index 000000000..7053d83a3 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/internal.go @@ -0,0 +1,170 @@ +package internal + +import ( + "fmt" + "strings" + "sync" + "time" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +const ( + DateTimeLayout = "_2-Jan-2006 15:04:05 -0700" + DateLayout = "2-Jan-2006" +) + +const FlagRecent imap.Flag = "\\Recent" // removed in IMAP4rev2 + +func DecodeDateTime(dec *imapwire.Decoder) (time.Time, error) { + var s string + if !dec.Quoted(&s) { + return time.Time{}, nil + } + t, err := time.Parse(DateTimeLayout, s) + if err != nil { + return time.Time{}, fmt.Errorf("in date-time: %v", err) // TODO: use imapwire.DecodeExpectError? + } + return t, err +} + +func ExpectDateTime(dec *imapwire.Decoder) (time.Time, error) { + t, err := DecodeDateTime(dec) + if err != nil { + return t, err + } + if !dec.Expect(!t.IsZero(), "date-time") { + return t, dec.Err() + } + return t, nil +} + +func ExpectDate(dec *imapwire.Decoder) (time.Time, error) { + var s string + if !dec.ExpectAString(&s) { + return time.Time{}, dec.Err() + } + t, err := time.Parse(DateLayout, s) + if err != nil { + return time.Time{}, fmt.Errorf("in date: %v", err) // use imapwire.DecodeExpectError? + } + return t, nil +} + +func ExpectFlagList(dec *imapwire.Decoder) ([]imap.Flag, error) { + var flags []imap.Flag + err := dec.ExpectList(func() error { + // Some servers start the list with a space, so we need to skip it + // https://github.com/emersion/go-imap/pull/633 + dec.SP() + + flag, err := ExpectFlag(dec) + if err != nil { + return err + } + flags = append(flags, flag) + return nil + }) + return flags, err +} + +func ExpectFlag(dec *imapwire.Decoder) (imap.Flag, error) { + isSystem := dec.Special('\\') + if isSystem && dec.Special('*') { + return imap.FlagWildcard, nil // flag-perm + } + var name string + if !dec.ExpectAtom(&name) { + return "", fmt.Errorf("in flag: %w", dec.Err()) + } + if isSystem { + name = "\\" + name + } + return canonicalFlag(name), nil +} + +func ExpectMailboxAttrList(dec *imapwire.Decoder) ([]imap.MailboxAttr, error) { + var attrs []imap.MailboxAttr + err := dec.ExpectList(func() error { + attr, err := ExpectMailboxAttr(dec) + if err != nil { + return err + } + attrs = append(attrs, attr) + return nil + }) + return attrs, err +} + +func ExpectMailboxAttr(dec *imapwire.Decoder) (imap.MailboxAttr, error) { + flag, err := ExpectFlag(dec) + return canonicalMailboxAttr(string(flag)), err +} + +var ( + canonOnce sync.Once + canonFlag map[string]imap.Flag + canonMailboxAttr map[string]imap.MailboxAttr +) + +func canonInit() { + flags := []imap.Flag{ + imap.FlagSeen, + imap.FlagAnswered, + imap.FlagFlagged, + imap.FlagDeleted, + imap.FlagDraft, + imap.FlagForwarded, + imap.FlagMDNSent, + imap.FlagJunk, + imap.FlagNotJunk, + imap.FlagPhishing, + imap.FlagImportant, + } + mailboxAttrs := []imap.MailboxAttr{ + imap.MailboxAttrNonExistent, + imap.MailboxAttrNoInferiors, + imap.MailboxAttrNoSelect, + imap.MailboxAttrHasChildren, + imap.MailboxAttrHasNoChildren, + imap.MailboxAttrMarked, + imap.MailboxAttrUnmarked, + imap.MailboxAttrSubscribed, + imap.MailboxAttrRemote, + imap.MailboxAttrAll, + imap.MailboxAttrArchive, + imap.MailboxAttrDrafts, + imap.MailboxAttrFlagged, + imap.MailboxAttrJunk, + imap.MailboxAttrSent, + imap.MailboxAttrTrash, + imap.MailboxAttrImportant, + } + + canonFlag = make(map[string]imap.Flag) + for _, flag := range flags { + canonFlag[strings.ToLower(string(flag))] = flag + } + + canonMailboxAttr = make(map[string]imap.MailboxAttr) + for _, attr := range mailboxAttrs { + canonMailboxAttr[strings.ToLower(string(attr))] = attr + } +} + +func canonicalFlag(s string) imap.Flag { + canonOnce.Do(canonInit) + if flag, ok := canonFlag[strings.ToLower(s)]; ok { + return flag + } + return imap.Flag(s) +} + +func canonicalMailboxAttr(s string) imap.MailboxAttr { + canonOnce.Do(canonInit) + if attr, ok := canonMailboxAttr[strings.ToLower(s)]; ok { + return attr + } + return imap.MailboxAttr(s) +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/sasl.go b/vendor/github.com/emersion/go-imap/v2/internal/sasl.go new file mode 100644 index 000000000..85d9f3d39 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/sasl.go @@ -0,0 +1,23 @@ +package internal + +import ( + "encoding/base64" +) + +func EncodeSASL(b []byte) string { + if len(b) == 0 { + return "=" + } else { + return base64.StdEncoding.EncodeToString(b) + } +} + +func DecodeSASL(s string) ([]byte, error) { + if s == "=" { + // go-sasl treats nil as no challenge/response, so return a non-nil + // empty byte slice + return []byte{}, nil + } else { + return base64.StdEncoding.DecodeString(s) + } +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/utf7/decoder.go b/vendor/github.com/emersion/go-imap/v2/internal/utf7/decoder.go new file mode 100644 index 000000000..b8e906e44 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/utf7/decoder.go @@ -0,0 +1,118 @@ +package utf7 + +import ( + "errors" + "strings" + "unicode/utf16" + "unicode/utf8" +) + +// ErrInvalidUTF7 means that a decoder encountered invalid UTF-7. +var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7") + +// Decode decodes a string encoded with modified UTF-7. +// +// Note, raw UTF-8 is accepted. +func Decode(src string) (string, error) { + if !utf8.ValidString(src) { + return "", errors.New("invalid UTF-8") + } + + var sb strings.Builder + sb.Grow(len(src)) + + ascii := true + for i := 0; i < len(src); i++ { + ch := src[i] + + if ch < min || (ch > max && ch < utf8.RuneSelf) { + // Illegal code point in ASCII mode. Note, UTF-8 codepoints are + // always allowed. + return "", ErrInvalidUTF7 + } + + if ch != '&' { + sb.WriteByte(ch) + ascii = true + continue + } + + // Find the end of the Base64 or "&-" segment + start := i + 1 + for i++; i < len(src) && src[i] != '-'; i++ { + if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF + return "", ErrInvalidUTF7 + } + } + + if i == len(src) { // Implicit shift ("&...") + return "", ErrInvalidUTF7 + } + + if i == start { // Escape sequence "&-" + sb.WriteByte('&') + ascii = true + } else { // Control or non-ASCII code points in base64 + if !ascii { // Null shift ("&...-&...-") + return "", ErrInvalidUTF7 + } + + b := decode([]byte(src[start:i])) + if len(b) == 0 { // Bad encoding + return "", ErrInvalidUTF7 + } + sb.Write(b) + + ascii = false + } + } + + return sb.String(), nil +} + +// Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8. +// A nil slice is returned if the encoding is invalid. +func decode(b64 []byte) []byte { + var b []byte + + // Allocate a single block of memory large enough to store the Base64 data + // (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes. + // Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence, + // double the space allocation for UTF-8. + if n := len(b64); b64[n-1] == '=' { + return nil + } else if n&3 == 0 { + b = make([]byte, b64Enc.DecodedLen(n)*3) + } else { + n += 4 - n&3 + b = make([]byte, n+b64Enc.DecodedLen(n)*3) + copy(b[copy(b, b64):n], []byte("==")) + b64, b = b[:n], b[n:] + } + + // Decode Base64 into the first 1/3rd of b + n, err := b64Enc.Decode(b, b64) + if err != nil || n&1 == 1 { + return nil + } + + // Decode UTF-16-BE into the remaining 2/3rds of b + b, s := b[:n], b[n:] + j := 0 + for i := 0; i < n; i += 2 { + r := rune(b[i])<<8 | rune(b[i+1]) + if utf16.IsSurrogate(r) { + if i += 2; i == n { + return nil + } + r2 := rune(b[i])<<8 | rune(b[i+1]) + if r = utf16.DecodeRune(r, r2); r == utf8.RuneError { + return nil + } + } else if min <= r && r <= max { + return nil + } + j += utf8.EncodeRune(s[j:], r) + } + return s[:j] +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/utf7/encoder.go b/vendor/github.com/emersion/go-imap/v2/internal/utf7/encoder.go new file mode 100644 index 000000000..e7107c320 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/utf7/encoder.go @@ -0,0 +1,88 @@ +package utf7 + +import ( + "strings" + "unicode/utf16" + "unicode/utf8" +) + +// Encode encodes a string with modified UTF-7. +func Encode(src string) string { + var sb strings.Builder + sb.Grow(len(src)) + + for i := 0; i < len(src); { + ch := src[i] + + if min <= ch && ch <= max { + sb.WriteByte(ch) + if ch == '&' { + sb.WriteByte('-') + } + + i++ + } else { + start := i + + // Find the next printable ASCII code point + i++ + for i < len(src) && (src[i] < min || src[i] > max) { + i++ + } + + sb.Write(encode([]byte(src[start:i]))) + } + } + + return sb.String() +} + +// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64, +// removes the padding, and adds UTF-7 shifts. +func encode(s []byte) []byte { + // len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no + // control code points (see table below). + b := make([]byte, 0, len(s)+4) + for len(s) > 0 { + r, size := utf8.DecodeRune(s) + if r > utf8.MaxRune { + r, size = utf8.RuneError, 1 // Bug fix (issue 3785) + } + s = s[size:] + if r1, r2 := utf16.EncodeRune(r); r1 != utf8.RuneError { + b = append(b, byte(r1>>8), byte(r1)) + r = r2 + } + b = append(b, byte(r>>8), byte(r)) + } + + // Encode as base64 + n := b64Enc.EncodedLen(len(b)) + 2 + b64 := make([]byte, n) + b64Enc.Encode(b64[1:], b) + + // Strip padding + n -= 2 - (len(b)+2)%3 + b64 = b64[:n] + + // Add UTF-7 shifts + b64[0] = '&' + b64[n-1] = '-' + return b64 +} + +// Escape passes through raw UTF-8 as-is and escapes the special UTF-7 marker +// (the ampersand character). +func Escape(src string) string { + var sb strings.Builder + sb.Grow(len(src)) + + for _, ch := range src { + sb.WriteRune(ch) + if ch == '&' { + sb.WriteByte('-') + } + } + + return sb.String() +} diff --git a/vendor/github.com/emersion/go-imap/v2/internal/utf7/utf7.go b/vendor/github.com/emersion/go-imap/v2/internal/utf7/utf7.go new file mode 100644 index 000000000..3ff09a968 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/internal/utf7/utf7.go @@ -0,0 +1,13 @@ +// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3 +package utf7 + +import ( + "encoding/base64" +) + +const ( + min = 0x20 // Minimum self-representing UTF-7 value + max = 0x7E // Maximum self-representing UTF-7 value +) + +var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,") diff --git a/vendor/github.com/emersion/go-imap/v2/list.go b/vendor/github.com/emersion/go-imap/v2/list.go new file mode 100644 index 000000000..a3103a60d --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/list.go @@ -0,0 +1,30 @@ +package imap + +// ListOptions contains options for the LIST command. +type ListOptions struct { + SelectSubscribed bool + SelectRemote bool + SelectRecursiveMatch bool // requires SelectSubscribed to be set + SelectSpecialUse bool // requires SPECIAL-USE + + ReturnSubscribed bool + ReturnChildren bool + ReturnStatus *StatusOptions // requires IMAP4rev2 or LIST-STATUS + ReturnSpecialUse bool // requires SPECIAL-USE +} + +// ListData is the mailbox data returned by a LIST command. +type ListData struct { + Attrs []MailboxAttr + Delim rune + Mailbox string + + // Extended data + ChildInfo *ListDataChildInfo + OldName string + Status *StatusData +} + +type ListDataChildInfo struct { + Subscribed bool +} diff --git a/vendor/github.com/emersion/go-imap/v2/namespace.go b/vendor/github.com/emersion/go-imap/v2/namespace.go new file mode 100644 index 000000000..e538a394e --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/namespace.go @@ -0,0 +1,14 @@ +package imap + +// NamespaceData is the data returned by the NAMESPACE command. +type NamespaceData struct { + Personal []NamespaceDescriptor + Other []NamespaceDescriptor + Shared []NamespaceDescriptor +} + +// NamespaceDescriptor describes a namespace. +type NamespaceDescriptor struct { + Prefix string + Delim rune +} diff --git a/vendor/github.com/emersion/go-imap/v2/numset.go b/vendor/github.com/emersion/go-imap/v2/numset.go new file mode 100644 index 000000000..a96b181a2 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/numset.go @@ -0,0 +1,149 @@ +package imap + +import ( + "unsafe" + + "github.com/emersion/go-imap/v2/internal/imapnum" +) + +// NumSet is a set of numbers identifying messages. NumSet is either a SeqSet +// or a UIDSet. +type NumSet interface { + // String returns the IMAP representation of the message number set. + String() string + // Dynamic returns true if the set contains "*" or "n:*" ranges or if the + // set represents the special SEARCHRES marker. + Dynamic() bool + + numSet() imapnum.Set +} + +var ( + _ NumSet = SeqSet(nil) + _ NumSet = UIDSet(nil) +) + +// SeqSet is a set of message sequence numbers. +type SeqSet []SeqRange + +// SeqSetNum returns a new SeqSet containing the specified sequence numbers. +func SeqSetNum(nums ...uint32) SeqSet { + var s SeqSet + s.AddNum(nums...) + return s +} + +func (s *SeqSet) numSetPtr() *imapnum.Set { + return (*imapnum.Set)(unsafe.Pointer(s)) +} + +func (s SeqSet) numSet() imapnum.Set { + return *s.numSetPtr() +} + +func (s SeqSet) String() string { + return s.numSet().String() +} + +func (s SeqSet) Dynamic() bool { + return s.numSet().Dynamic() +} + +// Contains returns true if the non-zero sequence number num is contained in +// the set. +func (s *SeqSet) Contains(num uint32) bool { + return s.numSet().Contains(num) +} + +// Nums returns a slice of all sequence numbers contained in the set. +func (s *SeqSet) Nums() ([]uint32, bool) { + return s.numSet().Nums() +} + +// AddNum inserts new sequence numbers into the set. The value 0 represents "*". +func (s *SeqSet) AddNum(nums ...uint32) { + s.numSetPtr().AddNum(nums...) +} + +// AddRange inserts a new range into the set. +func (s *SeqSet) AddRange(start, stop uint32) { + s.numSetPtr().AddRange(start, stop) +} + +// AddSet inserts all sequence numbers from other into s. +func (s *SeqSet) AddSet(other SeqSet) { + s.numSetPtr().AddSet(other.numSet()) +} + +// SeqRange is a range of message sequence numbers. +type SeqRange struct { + Start, Stop uint32 +} + +// UIDSet is a set of message UIDs. +type UIDSet []UIDRange + +// UIDSetNum returns a new UIDSet containing the specified UIDs. +func UIDSetNum(uids ...UID) UIDSet { + var s UIDSet + s.AddNum(uids...) + return s +} + +func (s *UIDSet) numSetPtr() *imapnum.Set { + return (*imapnum.Set)(unsafe.Pointer(s)) +} + +func (s UIDSet) numSet() imapnum.Set { + return *s.numSetPtr() +} + +func (s UIDSet) String() string { + if IsSearchRes(s) { + return "$" + } + return s.numSet().String() +} + +func (s UIDSet) Dynamic() bool { + return s.numSet().Dynamic() || IsSearchRes(s) +} + +// Contains returns true if the non-zero UID uid is contained in the set. +func (s UIDSet) Contains(uid UID) bool { + return s.numSet().Contains(uint32(uid)) +} + +// Nums returns a slice of all UIDs contained in the set. +func (s UIDSet) Nums() ([]UID, bool) { + nums, ok := s.numSet().Nums() + return uidListFromNumList(nums), ok +} + +// AddNum inserts new UIDs into the set. The value 0 represents "*". +func (s *UIDSet) AddNum(uids ...UID) { + s.numSetPtr().AddNum(numListFromUIDList(uids)...) +} + +// AddRange inserts a new range into the set. +func (s *UIDSet) AddRange(start, stop UID) { + s.numSetPtr().AddRange(uint32(start), uint32(stop)) +} + +// AddSet inserts all UIDs from other into s. +func (s *UIDSet) AddSet(other UIDSet) { + s.numSetPtr().AddSet(other.numSet()) +} + +// UIDRange is a range of message UIDs. +type UIDRange struct { + Start, Stop UID +} + +func numListFromUIDList(uids []UID) []uint32 { + return *(*[]uint32)(unsafe.Pointer(&uids)) +} + +func uidListFromNumList(nums []uint32) []UID { + return *(*[]UID)(unsafe.Pointer(&nums)) +} diff --git a/vendor/github.com/emersion/go-imap/v2/quota.go b/vendor/github.com/emersion/go-imap/v2/quota.go new file mode 100644 index 000000000..f128fe44f --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/quota.go @@ -0,0 +1,13 @@ +package imap + +// QuotaResourceType is a QUOTA resource type. +// +// See RFC 9208 section 5. +type QuotaResourceType string + +const ( + QuotaResourceStorage QuotaResourceType = "STORAGE" + QuotaResourceMessage QuotaResourceType = "MESSAGE" + QuotaResourceMailbox QuotaResourceType = "MAILBOX" + QuotaResourceAnnotationStorage QuotaResourceType = "ANNOTATION-STORAGE" +) diff --git a/vendor/github.com/emersion/go-imap/v2/response.go b/vendor/github.com/emersion/go-imap/v2/response.go new file mode 100644 index 000000000..0ce54cf67 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/response.go @@ -0,0 +1,81 @@ +package imap + +import ( + "fmt" + "strings" +) + +// StatusResponseType is a generic status response type. +type StatusResponseType string + +const ( + StatusResponseTypeOK StatusResponseType = "OK" + StatusResponseTypeNo StatusResponseType = "NO" + StatusResponseTypeBad StatusResponseType = "BAD" + StatusResponseTypePreAuth StatusResponseType = "PREAUTH" + StatusResponseTypeBye StatusResponseType = "BYE" +) + +// ResponseCode is a response code. +type ResponseCode string + +const ( + ResponseCodeAlert ResponseCode = "ALERT" + ResponseCodeAlreadyExists ResponseCode = "ALREADYEXISTS" + ResponseCodeAuthenticationFailed ResponseCode = "AUTHENTICATIONFAILED" + ResponseCodeAuthorizationFailed ResponseCode = "AUTHORIZATIONFAILED" + ResponseCodeBadCharset ResponseCode = "BADCHARSET" + ResponseCodeCannot ResponseCode = "CANNOT" + ResponseCodeClientBug ResponseCode = "CLIENTBUG" + ResponseCodeContactAdmin ResponseCode = "CONTACTADMIN" + ResponseCodeCorruption ResponseCode = "CORRUPTION" + ResponseCodeExpired ResponseCode = "EXPIRED" + ResponseCodeHasChildren ResponseCode = "HASCHILDREN" + ResponseCodeInUse ResponseCode = "INUSE" + ResponseCodeLimit ResponseCode = "LIMIT" + ResponseCodeNonExistent ResponseCode = "NONEXISTENT" + ResponseCodeNoPerm ResponseCode = "NOPERM" + ResponseCodeOverQuota ResponseCode = "OVERQUOTA" + ResponseCodeParse ResponseCode = "PARSE" + ResponseCodePrivacyRequired ResponseCode = "PRIVACYREQUIRED" + ResponseCodeServerBug ResponseCode = "SERVERBUG" + ResponseCodeTryCreate ResponseCode = "TRYCREATE" + ResponseCodeUnavailable ResponseCode = "UNAVAILABLE" + ResponseCodeUnknownCTE ResponseCode = "UNKNOWN-CTE" + + // METADATA + ResponseCodeTooMany ResponseCode = "TOOMANY" + ResponseCodeNoPrivate ResponseCode = "NOPRIVATE" + + // APPENDLIMIT + ResponseCodeTooBig ResponseCode = "TOOBIG" +) + +// StatusResponse is a generic status response. +// +// See RFC 9051 section 7.1. +type StatusResponse struct { + Type StatusResponseType + Code ResponseCode + Text string +} + +// Error is an IMAP error caused by a status response. +type Error StatusResponse + +var _ error = (*Error)(nil) + +// Error implements the error interface. +func (err *Error) Error() string { + var sb strings.Builder + fmt.Fprintf(&sb, "imap: %v", err.Type) + if err.Code != "" { + fmt.Fprintf(&sb, " [%v]", err.Code) + } + text := err.Text + if text == "" { + text = "" + } + fmt.Fprintf(&sb, " %v", text) + return sb.String() +} diff --git a/vendor/github.com/emersion/go-imap/v2/search.go b/vendor/github.com/emersion/go-imap/v2/search.go new file mode 100644 index 000000000..e5b77205e --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/search.go @@ -0,0 +1,202 @@ +package imap + +import ( + "reflect" + "time" +) + +// SearchOptions contains options for the SEARCH command. +type SearchOptions struct { + // Requires IMAP4rev2 or ESEARCH + ReturnMin bool + ReturnMax bool + ReturnAll bool + ReturnCount bool + // Requires IMAP4rev2 or SEARCHRES + ReturnSave bool +} + +// SearchCriteria is a criteria for the SEARCH command. +// +// When multiple fields are populated, the result is the intersection ("and" +// function) of all messages that match the fields. +// +// And, Not and Or can be used to combine multiple criteria together. For +// instance, the following criteria matches messages not containing "hello": +// +// SearchCriteria{Not: []SearchCriteria{{ +// Body: []string{"hello"}, +// }}} +// +// The following criteria matches messages containing either "hello" or +// "world": +// +// SearchCriteria{Or: [][2]SearchCriteria{{ +// {Body: []string{"hello"}}, +// {Body: []string{"world"}}, +// }}} +type SearchCriteria struct { + SeqNum []SeqSet + UID []UIDSet + + // Only the date is used, the time and timezone are ignored + Since time.Time + Before time.Time + SentSince time.Time + SentBefore time.Time + + Header []SearchCriteriaHeaderField + Body []string + Text []string + + Flag []Flag + NotFlag []Flag + + Larger int64 + Smaller int64 + + Not []SearchCriteria + Or [][2]SearchCriteria + + ModSeq *SearchCriteriaModSeq // requires CONDSTORE +} + +// And intersects two search criteria. +func (criteria *SearchCriteria) And(other *SearchCriteria) { + criteria.SeqNum = append(criteria.SeqNum, other.SeqNum...) + criteria.UID = append(criteria.UID, other.UID...) + + criteria.Since = intersectSince(criteria.Since, other.Since) + criteria.Before = intersectBefore(criteria.Before, other.Before) + criteria.SentSince = intersectSince(criteria.SentSince, other.SentSince) + criteria.SentBefore = intersectBefore(criteria.SentBefore, other.SentBefore) + + criteria.Header = append(criteria.Header, other.Header...) + criteria.Body = append(criteria.Body, other.Body...) + criteria.Text = append(criteria.Text, other.Text...) + + criteria.Flag = append(criteria.Flag, other.Flag...) + criteria.NotFlag = append(criteria.NotFlag, other.NotFlag...) + + if criteria.Larger == 0 || other.Larger > criteria.Larger { + criteria.Larger = other.Larger + } + if criteria.Smaller == 0 || other.Smaller < criteria.Smaller { + criteria.Smaller = other.Smaller + } + + criteria.Not = append(criteria.Not, other.Not...) + criteria.Or = append(criteria.Or, other.Or...) +} + +func intersectSince(t1, t2 time.Time) time.Time { + switch { + case t1.IsZero(): + return t2 + case t2.IsZero(): + return t1 + case t1.After(t2): + return t1 + default: + return t2 + } +} + +func intersectBefore(t1, t2 time.Time) time.Time { + switch { + case t1.IsZero(): + return t2 + case t2.IsZero(): + return t1 + case t1.Before(t2): + return t1 + default: + return t2 + } +} + +type SearchCriteriaHeaderField struct { + Key, Value string +} + +type SearchCriteriaModSeq struct { + ModSeq uint64 + MetadataName string + MetadataType SearchCriteriaMetadataType +} + +type SearchCriteriaMetadataType string + +const ( + SearchCriteriaMetadataAll SearchCriteriaMetadataType = "all" + SearchCriteriaMetadataPrivate SearchCriteriaMetadataType = "priv" + SearchCriteriaMetadataShared SearchCriteriaMetadataType = "shared" +) + +// SearchData is the data returned by a SEARCH command. +type SearchData struct { + All NumSet + + // requires IMAP4rev2 or ESEARCH + UID bool + Min uint32 + Max uint32 + Count uint32 + + // requires CONDSTORE + ModSeq uint64 +} + +// AllSeqNums returns All as a slice of sequence numbers. +func (data *SearchData) AllSeqNums() []uint32 { + seqSet, ok := data.All.(SeqSet) + if !ok { + return nil + } + + // Note: a dynamic sequence set would be a server bug + nums, ok := seqSet.Nums() + if !ok { + panic("imap: SearchData.All is a dynamic number set") + } + return nums +} + +// AllUIDs returns All as a slice of UIDs. +func (data *SearchData) AllUIDs() []UID { + uidSet, ok := data.All.(UIDSet) + if !ok { + return nil + } + + // Note: a dynamic sequence set would be a server bug + uids, ok := uidSet.Nums() + if !ok { + panic("imap: SearchData.All is a dynamic number set") + } + return uids +} + +// searchRes is a special empty UIDSet which can be used as a marker. It has +// a non-zero cap so that its data pointer is non-nil and can be compared. +// +// It's a UIDSet rather than a SeqSet so that it can be passed to the +// UID EXPUNGE command. +var ( + searchRes = make(UIDSet, 0, 1) + searchResAddr = reflect.ValueOf(searchRes).Pointer() +) + +// SearchRes returns a special marker which can be used instead of a UIDSet to +// reference the last SEARCH result. On the wire, it's encoded as '$'. +// +// It requires IMAP4rev2 or the SEARCHRES extension. +func SearchRes() UIDSet { + return searchRes +} + +// IsSearchRes checks whether a sequence set is a reference to the last SEARCH +// result. See SearchRes. +func IsSearchRes(numSet NumSet) bool { + return reflect.ValueOf(numSet).Pointer() == searchResAddr +} diff --git a/vendor/github.com/emersion/go-imap/v2/select.go b/vendor/github.com/emersion/go-imap/v2/select.go new file mode 100644 index 000000000..9e3359f50 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/select.go @@ -0,0 +1,25 @@ +package imap + +// SelectOptions contains options for the SELECT or EXAMINE command. +type SelectOptions struct { + ReadOnly bool + CondStore bool // requires CONDSTORE +} + +// SelectData is the data returned by a SELECT command. +// +// In the old RFC 2060, PermanentFlags, UIDNext and UIDValidity are optional. +type SelectData struct { + // Flags defined for this mailbox + Flags []Flag + // Flags that the client can change permanently + PermanentFlags []Flag + // Number of messages in this mailbox (aka. "EXISTS") + NumMessages uint32 + UIDNext UID + UIDValidity uint32 + + List *ListData // requires IMAP4rev2 + + HighestModSeq uint64 // requires CONDSTORE +} diff --git a/vendor/github.com/emersion/go-imap/v2/status.go b/vendor/github.com/emersion/go-imap/v2/status.go new file mode 100644 index 000000000..223eb6d70 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/status.go @@ -0,0 +1,33 @@ +package imap + +// StatusOptions contains options for the STATUS command. +type StatusOptions struct { + NumMessages bool + UIDNext bool + UIDValidity bool + NumUnseen bool + NumDeleted bool // requires IMAP4rev2 or QUOTA + Size bool // requires IMAP4rev2 or STATUS=SIZE + + AppendLimit bool // requires APPENDLIMIT + DeletedStorage bool // requires QUOTA=RES-STORAGE + HighestModSeq bool // requires CONDSTORE +} + +// StatusData is the data returned by a STATUS command. +// +// The mailbox name is always populated. The remaining fields are optional. +type StatusData struct { + Mailbox string + + NumMessages *uint32 + UIDNext UID + UIDValidity uint32 + NumUnseen *uint32 + NumDeleted *uint32 + Size *int64 + + AppendLimit *uint32 + DeletedStorage *int64 + HighestModSeq uint64 +} diff --git a/vendor/github.com/emersion/go-imap/v2/store.go b/vendor/github.com/emersion/go-imap/v2/store.go new file mode 100644 index 000000000..c1ea26fa5 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/store.go @@ -0,0 +1,22 @@ +package imap + +// StoreOptions contains options for the STORE command. +type StoreOptions struct { + UnchangedSince uint64 // requires CONDSTORE +} + +// StoreFlagsOp is a flag operation: set, add or delete. +type StoreFlagsOp int + +const ( + StoreFlagsSet StoreFlagsOp = iota + StoreFlagsAdd + StoreFlagsDel +) + +// StoreFlags alters message flags. +type StoreFlags struct { + Op StoreFlagsOp + Silent bool + Flags []Flag +} diff --git a/vendor/github.com/emersion/go-imap/v2/thread.go b/vendor/github.com/emersion/go-imap/v2/thread.go new file mode 100644 index 000000000..e4e3122ed --- /dev/null +++ b/vendor/github.com/emersion/go-imap/v2/thread.go @@ -0,0 +1,9 @@ +package imap + +// ThreadAlgorithm is a threading algorithm. +type ThreadAlgorithm string + +const ( + ThreadOrderedSubject ThreadAlgorithm = "ORDEREDSUBJECT" + ThreadReferences ThreadAlgorithm = "REFERENCES" +) diff --git a/vendor/github.com/emersion/go-message/.build.yml b/vendor/github.com/emersion/go-message/.build.yml new file mode 100644 index 000000000..cc583f262 --- /dev/null +++ b/vendor/github.com/emersion/go-message/.build.yml @@ -0,0 +1,20 @@ +image: alpine/latest +packages: + - go +sources: + - https://github.com/emersion/go-message +artifacts: + - coverage.html +tasks: + - build: | + cd go-message + go build -v ./... + - test: | + cd go-message + go test -coverprofile=coverage.txt -covermode=atomic ./... + - coverage: | + cd go-message + go tool cover -html=coverage.txt -o ~/coverage.html + - gofmt: | + cd go-message + test -z $(gofmt -l .) diff --git a/vendor/github.com/emersion/go-message/.gitignore b/vendor/github.com/emersion/go-message/.gitignore new file mode 100644 index 000000000..daf913b1b --- /dev/null +++ b/vendor/github.com/emersion/go-message/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/emersion/go-message/LICENSE b/vendor/github.com/emersion/go-message/LICENSE new file mode 100644 index 000000000..0d504877b --- /dev/null +++ b/vendor/github.com/emersion/go-message/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 emersion + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/emersion/go-message/README.md b/vendor/github.com/emersion/go-message/README.md new file mode 100644 index 000000000..20bf89142 --- /dev/null +++ b/vendor/github.com/emersion/go-message/README.md @@ -0,0 +1,31 @@ +# go-message + +[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-message.svg)](https://pkg.go.dev/github.com/emersion/go-message) +[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-message/commits/master.svg)](https://builds.sr.ht/~emersion/go-message/commits/master?) + +A Go library for the Internet Message Format. It implements: + +* [RFC 5322]: Internet Message Format +* [RFC 2045], [RFC 2046] and [RFC 2047]: Multipurpose Internet Mail Extensions +* [RFC 2183]: Content-Disposition Header Field + +## Features + +* Streaming API +* Automatic encoding and charset handling (to decode all charsets, add + `import _ "github.com/emersion/go-message/charset"` to your application) +* A [`mail`](https://godocs.io/github.com/emersion/go-message/mail) subpackage + to read and write mail messages +* DKIM-friendly +* A [`textproto`](https://godocs.io/github.com/emersion/go-message/textproto) + subpackage that just implements the wire format + +## License + +MIT + +[RFC 5322]: https://tools.ietf.org/html/rfc5322 +[RFC 2045]: https://tools.ietf.org/html/rfc2045 +[RFC 2046]: https://tools.ietf.org/html/rfc2046 +[RFC 2047]: https://tools.ietf.org/html/rfc2047 +[RFC 2183]: https://tools.ietf.org/html/rfc2183 diff --git a/vendor/github.com/emersion/go-message/charset.go b/vendor/github.com/emersion/go-message/charset.go new file mode 100644 index 000000000..9d4d10e72 --- /dev/null +++ b/vendor/github.com/emersion/go-message/charset.go @@ -0,0 +1,66 @@ +package message + +import ( + "errors" + "fmt" + "io" + "mime" + "strings" +) + +type UnknownCharsetError struct { + e error +} + +func (u UnknownCharsetError) Unwrap() error { return u.e } + +func (u UnknownCharsetError) Error() string { + return "unknown charset: " + u.e.Error() +} + +// IsUnknownCharset returns a boolean indicating whether the error is known to +// report that the charset advertised by the entity is unknown. +func IsUnknownCharset(err error) bool { + return errors.As(err, new(UnknownCharsetError)) +} + +// CharsetReader, if non-nil, defines a function to generate charset-conversion +// readers, converting from the provided charset into UTF-8. Charsets are always +// lower-case. utf-8 and us-ascii charsets are handled by default. One of the +// the CharsetReader's result values must be non-nil. +// +// Importing github.com/emersion/go-message/charset will set CharsetReader to +// a function that handles most common charsets. Alternatively, CharsetReader +// can be set to e.g. golang.org/x/net/html/charset.NewReaderLabel. +var CharsetReader func(charset string, input io.Reader) (io.Reader, error) + +// charsetReader calls CharsetReader if non-nil. +func charsetReader(charset string, input io.Reader) (io.Reader, error) { + charset = strings.ToLower(charset) + if charset == "utf-8" || charset == "us-ascii" { + return input, nil + } + if CharsetReader != nil { + r, err := CharsetReader(charset, input) + if err != nil { + return r, UnknownCharsetError{err} + } + return r, nil + } + return input, UnknownCharsetError{fmt.Errorf("message: unhandled charset %q", charset)} +} + +// decodeHeader decodes an internationalized header field. If it fails, it +// returns the input string and the error. +func decodeHeader(s string) (string, error) { + wordDecoder := mime.WordDecoder{CharsetReader: charsetReader} + dec, err := wordDecoder.DecodeHeader(s) + if err != nil { + return s, err + } + return dec, nil +} + +func encodeHeader(s string) string { + return mime.QEncoding.Encode("utf-8", s) +} diff --git a/vendor/github.com/emersion/go-message/encoding.go b/vendor/github.com/emersion/go-message/encoding.go new file mode 100644 index 000000000..a503276d4 --- /dev/null +++ b/vendor/github.com/emersion/go-message/encoding.go @@ -0,0 +1,151 @@ +package message + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "mime/quotedprintable" + "strings" +) + +type UnknownEncodingError struct { + e error +} + +func (u UnknownEncodingError) Unwrap() error { return u.e } + +func (u UnknownEncodingError) Error() string { + return "encoding error: " + u.e.Error() +} + +// IsUnknownEncoding returns a boolean indicating whether the error is known to +// report that the encoding advertised by the entity is unknown. +func IsUnknownEncoding(err error) bool { + return errors.As(err, new(UnknownEncodingError)) +} + +func encodingReader(enc string, r io.Reader) (io.Reader, error) { + var dec io.Reader + switch strings.ToLower(enc) { + case "quoted-printable": + dec = quotedprintable.NewReader(r) + case "base64": + wrapped := &whitespaceReplacingReader{wrapped: r} + dec = base64.NewDecoder(base64.StdEncoding, wrapped) + case "7bit", "8bit", "binary", "": + dec = r + default: + return nil, fmt.Errorf("unhandled encoding %q", enc) + } + return dec, nil +} + +type nopCloser struct { + io.Writer +} + +func (nopCloser) Close() error { + return nil +} + +func encodingWriter(enc string, w io.Writer) (io.WriteCloser, error) { + var wc io.WriteCloser + switch strings.ToLower(enc) { + case "quoted-printable": + wc = quotedprintable.NewWriter(w) + case "base64": + wc = base64.NewEncoder(base64.StdEncoding, &lineWrapper{w: w, maxLineLen: 76}) + case "7bit", "8bit": + wc = nopCloser{&lineWrapper{w: w, maxLineLen: 998}} + case "binary", "": + wc = nopCloser{w} + default: + return nil, fmt.Errorf("unhandled encoding %q", enc) + } + return wc, nil +} + +// whitespaceReplacingReader replaces space and tab characters with a LF so +// base64 bodies with a continuation indent can be decoded by the base64 decoder +// even though it is against the spec. +type whitespaceReplacingReader struct { + wrapped io.Reader +} + +func (r *whitespaceReplacingReader) Read(p []byte) (int, error) { + n, err := r.wrapped.Read(p) + + for i := 0; i < n; i++ { + if p[i] == ' ' || p[i] == '\t' { + p[i] = '\n' + } + } + + return n, err +} + +type lineWrapper struct { + w io.Writer + maxLineLen int + + curLineLen int + cr bool +} + +func (w *lineWrapper) Write(b []byte) (int, error) { + var written int + for len(b) > 0 { + var l []byte + l, b = cutLine(b, w.maxLineLen-w.curLineLen) + + lf := bytes.HasSuffix(l, []byte("\n")) + l = bytes.TrimSuffix(l, []byte("\n")) + + n, err := w.w.Write(l) + if err != nil { + return written, err + } + written += n + + cr := bytes.HasSuffix(l, []byte("\r")) + if len(l) == 0 { + cr = w.cr + } + + if !lf && len(b) == 0 { + w.curLineLen += len(l) + w.cr = cr + break + } + w.curLineLen = 0 + + ending := []byte("\r\n") + if cr { + ending = []byte("\n") + } + _, err = w.w.Write(ending) + if err != nil { + return written, err + } + w.cr = false + } + + return written, nil +} + +func cutLine(b []byte, max int) ([]byte, []byte) { + for i := 0; i < len(b); i++ { + if b[i] == '\r' && i == max { + continue + } + if b[i] == '\n' { + return b[:i+1], b[i+1:] + } + if i >= max { + return b[:i], b[i:] + } + } + return b, nil +} diff --git a/vendor/github.com/emersion/go-message/entity.go b/vendor/github.com/emersion/go-message/entity.go new file mode 100644 index 000000000..ee2ebe6a0 --- /dev/null +++ b/vendor/github.com/emersion/go-message/entity.go @@ -0,0 +1,264 @@ +package message + +import ( + "bufio" + "errors" + "io" + "math" + "strings" + + "github.com/emersion/go-message/textproto" +) + +// An Entity is either a whole message or a one of the parts in the body of a +// multipart entity. +type Entity struct { + Header Header // The entity's header. + Body io.Reader // The decoded entity's body. + + mediaType string + mediaParams map[string]string +} + +// New makes a new message with the provided header and body. The entity's +// transfer encoding and charset are automatically decoded to UTF-8. +// +// If the message uses an unknown transfer encoding or charset, New returns an +// error that verifies IsUnknownCharset, but also returns an Entity that can +// be read. +func New(header Header, body io.Reader) (*Entity, error) { + var err error + + mediaType, mediaParams, _ := header.ContentType() + + // QUIRK: RFC 2045 section 6.4 specifies that multipart messages can't have + // a Content-Transfer-Encoding other than "7bit", "8bit" or "binary". + // However some messages in the wild are non-conformant and have it set to + // e.g. "quoted-printable". So we just ignore it for multipart. + // See https://github.com/emersion/go-message/issues/48 + if !strings.HasPrefix(mediaType, "multipart/") { + enc := header.Get("Content-Transfer-Encoding") + if decoded, encErr := encodingReader(enc, body); encErr != nil { + err = UnknownEncodingError{encErr} + } else { + body = decoded + } + } + + // RFC 2046 section 4.1.2: charset only applies to text/* + if strings.HasPrefix(mediaType, "text/") { + if ch, ok := mediaParams["charset"]; ok { + if converted, charsetErr := charsetReader(ch, body); charsetErr != nil { + err = UnknownCharsetError{charsetErr} + } else { + body = converted + } + } + } + + return &Entity{ + Header: header, + Body: body, + mediaType: mediaType, + mediaParams: mediaParams, + }, err +} + +// NewMultipart makes a new multipart message with the provided header and +// parts. The Content-Type header must begin with "multipart/". +// +// If the message uses an unknown transfer encoding, NewMultipart returns an +// error that verifies IsUnknownCharset, but also returns an Entity that can +// be read. +func NewMultipart(header Header, parts []*Entity) (*Entity, error) { + r := &multipartBody{ + header: header, + parts: parts, + } + + return New(header, r) +} + +const defaultMaxHeaderBytes = 1 << 20 // 1 MB + +var errHeaderTooBig = errors.New("message: header exceeds maximum size") + +// limitedReader is the same as io.LimitedReader, but returns a custom error. +type limitedReader struct { + R io.Reader + N int64 +} + +func (lr *limitedReader) Read(p []byte) (int, error) { + if lr.N <= 0 { + return 0, errHeaderTooBig + } + if int64(len(p)) > lr.N { + p = p[0:lr.N] + } + n, err := lr.R.Read(p) + lr.N -= int64(n) + return n, err +} + +// ReadOptions are options for ReadWithOptions. +type ReadOptions struct { + // MaxHeaderBytes limits the maximum permissible size of a message header + // block. If exceeded, an error will be returned. + // + // Set to -1 for no limit, set to 0 for the default value (1MB). + MaxHeaderBytes int64 +} + +// withDefaults returns a sanitised version of the options with defaults/special +// values accounted for. +func (o *ReadOptions) withDefaults() *ReadOptions { + var out ReadOptions + if o != nil { + out = *o + } + if out.MaxHeaderBytes == 0 { + out.MaxHeaderBytes = defaultMaxHeaderBytes + } else if out.MaxHeaderBytes < 0 { + out.MaxHeaderBytes = math.MaxInt64 + } + return &out +} + +// ReadWithOptions see Read, but allows overriding some parameters with +// ReadOptions. +// +// If the message uses an unknown transfer encoding or charset, ReadWithOptions +// returns an error that verifies IsUnknownCharset or IsUnknownEncoding, but +// also returns an Entity that can be read. +func ReadWithOptions(r io.Reader, opts *ReadOptions) (*Entity, error) { + opts = opts.withDefaults() + + lr := &limitedReader{R: r, N: opts.MaxHeaderBytes} + br := bufio.NewReader(lr) + + h, err := textproto.ReadHeader(br) + if err != nil { + return nil, err + } + + lr.N = math.MaxInt64 + + return New(Header{h}, br) +} + +// Read reads a message from r. The message's encoding and charset are +// automatically decoded to raw UTF-8. Note that this function only reads the +// message header. +// +// If the message uses an unknown transfer encoding or charset, Read returns an +// error that verifies IsUnknownCharset or IsUnknownEncoding, but also returns +// an Entity that can be read. +func Read(r io.Reader) (*Entity, error) { + return ReadWithOptions(r, nil) +} + +// MultipartReader returns a MultipartReader that reads parts from this entity's +// body. If this entity is not multipart, it returns nil. +func (e *Entity) MultipartReader() MultipartReader { + if !strings.HasPrefix(e.mediaType, "multipart/") { + return nil + } + if mb, ok := e.Body.(*multipartBody); ok { + return mb + } + return &multipartReader{textproto.NewMultipartReader(e.Body, e.mediaParams["boundary"])} +} + +// writeBodyTo writes this entity's body to w (without the header). +func (e *Entity) writeBodyTo(w *Writer) error { + var err error + if mb, ok := e.Body.(*multipartBody); ok { + err = mb.writeBodyTo(w) + } else { + _, err = io.Copy(w, e.Body) + } + return err +} + +// WriteTo writes this entity's header and body to w. +func (e *Entity) WriteTo(w io.Writer) error { + ew, err := CreateWriter(w, e.Header) + if err != nil { + return err + } + + if err := e.writeBodyTo(ew); err != nil { + ew.Close() + return err + } + + return ew.Close() +} + +// WalkFunc is the type of the function called for each part visited by Walk. +// +// The path argument is a list of multipart indices leading to the part. The +// root part has a nil path. +// +// If there was an encoding error walking to a part, the incoming error will +// describe the problem and the function can decide how to handle that error. +// +// Unlike IMAP part paths, indices start from 0 (instead of 1) and a +// non-multipart message has a nil path (instead of {1}). +// +// If an error is returned, processing stops. +type WalkFunc func(path []int, entity *Entity, err error) error + +// Walk walks the entity's multipart tree, calling walkFunc for each part in +// the tree, including the root entity. +// +// Walk consumes the entity. +func (e *Entity) Walk(walkFunc WalkFunc) error { + var multipartReaders []MultipartReader + var path []int + part := e + for { + var err error + if part == nil { + if len(multipartReaders) == 0 { + break + } + + // Get the next part from the last multipart reader + mr := multipartReaders[len(multipartReaders)-1] + part, err = mr.NextPart() + if err == io.EOF { + multipartReaders = multipartReaders[:len(multipartReaders)-1] + path = path[:len(path)-1] + continue + } else if IsUnknownEncoding(err) || IsUnknownCharset(err) { + // Forward the error to walkFunc + } else if err != nil { + return err + } + + path[len(path)-1]++ + } + + // Copy the path since we'll mutate it on the next iteration + var pathCopy []int + if len(path) > 0 { + pathCopy = make([]int, len(path)) + copy(pathCopy, path) + } + + if err := walkFunc(pathCopy, part, err); err != nil { + return err + } + + if mr := part.MultipartReader(); mr != nil { + multipartReaders = append(multipartReaders, mr) + path = append(path, -1) + } + + part = nil + } + + return nil +} diff --git a/vendor/github.com/emersion/go-message/header.go b/vendor/github.com/emersion/go-message/header.go new file mode 100644 index 000000000..1a98fe660 --- /dev/null +++ b/vendor/github.com/emersion/go-message/header.go @@ -0,0 +1,118 @@ +package message + +import ( + "mime" + + "github.com/emersion/go-message/textproto" +) + +func parseHeaderWithParams(s string) (f string, params map[string]string, err error) { + f, params, err = mime.ParseMediaType(s) + if err != nil { + return s, nil, err + } + for k, v := range params { + params[k], _ = decodeHeader(v) + } + return +} + +func formatHeaderWithParams(f string, params map[string]string) string { + encParams := make(map[string]string) + for k, v := range params { + encParams[k] = encodeHeader(v) + } + return mime.FormatMediaType(f, encParams) +} + +// HeaderFields iterates over header fields. +type HeaderFields interface { + textproto.HeaderFields + + // Text parses the value of the current field as plaintext. The field + // charset is decoded to UTF-8. If the header field's charset is unknown, + // the raw field value is returned and the error verifies IsUnknownCharset. + Text() (string, error) +} + +type headerFields struct { + textproto.HeaderFields +} + +func (hf *headerFields) Text() (string, error) { + return decodeHeader(hf.Value()) +} + +// A Header represents the key-value pairs in a message header. +type Header struct { + textproto.Header +} + +// HeaderFromMap creates a header from a map of header fields. +// +// This function is provided for interoperability with the standard library. +// If possible, ReadHeader should be used instead to avoid loosing information. +// The map representation looses the ordering of the fields, the capitalization +// of the header keys, and the whitespace of the original header. +func HeaderFromMap(m map[string][]string) Header { + return Header{textproto.HeaderFromMap(m)} +} + +// ContentType parses the Content-Type header field. +// +// If no Content-Type is specified, it returns "text/plain". +func (h *Header) ContentType() (t string, params map[string]string, err error) { + v := h.Get("Content-Type") + if v == "" { + return "text/plain", nil, nil + } + return parseHeaderWithParams(v) +} + +// SetContentType formats the Content-Type header field. +func (h *Header) SetContentType(t string, params map[string]string) { + h.Set("Content-Type", formatHeaderWithParams(t, params)) +} + +// ContentDisposition parses the Content-Disposition header field, as defined in +// RFC 2183. +func (h *Header) ContentDisposition() (disp string, params map[string]string, err error) { + return parseHeaderWithParams(h.Get("Content-Disposition")) +} + +// SetContentDisposition formats the Content-Disposition header field, as +// defined in RFC 2183. +func (h *Header) SetContentDisposition(disp string, params map[string]string) { + h.Set("Content-Disposition", formatHeaderWithParams(disp, params)) +} + +// Text parses a plaintext header field. The field charset is automatically +// decoded to UTF-8. If the header field's charset is unknown, the raw field +// value is returned and the error verifies IsUnknownCharset. +func (h *Header) Text(k string) (string, error) { + return decodeHeader(h.Get(k)) +} + +// SetText sets a plaintext header field. +func (h *Header) SetText(k, v string) { + h.Set(k, encodeHeader(v)) +} + +// Copy creates a stand-alone copy of the header. +func (h *Header) Copy() Header { + return Header{h.Header.Copy()} +} + +// Fields iterates over all the header fields. +// +// The header may not be mutated while iterating, except using HeaderFields.Del. +func (h *Header) Fields() HeaderFields { + return &headerFields{h.Header.Fields()} +} + +// FieldsByKey iterates over all fields having the specified key. +// +// The header may not be mutated while iterating, except using HeaderFields.Del. +func (h *Header) FieldsByKey(k string) HeaderFields { + return &headerFields{h.Header.FieldsByKey(k)} +} diff --git a/vendor/github.com/emersion/go-message/mail/address.go b/vendor/github.com/emersion/go-message/mail/address.go new file mode 100644 index 000000000..3d3bbca11 --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/address.go @@ -0,0 +1,42 @@ +package mail + +import ( + "mime" + "net/mail" + "strings" + + "github.com/emersion/go-message" +) + +// Address represents a single mail address. +// The type alias ensures that a net/mail.Address can be used wherever an +// Address is expected +type Address = mail.Address + +func formatAddressList(l []*Address) string { + formatted := make([]string, len(l)) + for i, a := range l { + formatted[i] = a.String() + } + return strings.Join(formatted, ", ") +} + +// ParseAddress parses a single RFC 5322 address, e.g. "Barry Gibbs " +// Use this function only if you parse from a string, if you have a Header use +// Header.AddressList instead +func ParseAddress(address string) (*Address, error) { + parser := mail.AddressParser{ + &mime.WordDecoder{message.CharsetReader}, + } + return parser.Parse(address) +} + +// ParseAddressList parses the given string as a list of addresses. +// Use this function only if you parse from a string, if you have a Header use +// Header.AddressList instead +func ParseAddressList(list string) ([]*Address, error) { + parser := mail.AddressParser{ + &mime.WordDecoder{message.CharsetReader}, + } + return parser.ParseList(list) +} diff --git a/vendor/github.com/emersion/go-message/mail/attachment.go b/vendor/github.com/emersion/go-message/mail/attachment.go new file mode 100644 index 000000000..3fbbce266 --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/attachment.go @@ -0,0 +1,30 @@ +package mail + +import ( + "github.com/emersion/go-message" +) + +// An AttachmentHeader represents an attachment's header. +type AttachmentHeader struct { + message.Header +} + +// Filename parses the attachment's filename. +func (h *AttachmentHeader) Filename() (string, error) { + _, params, err := h.ContentDisposition() + + filename, ok := params["filename"] + if !ok { + // Using "name" in Content-Type is discouraged + _, params, err = h.ContentType() + filename = params["name"] + } + + return filename, err +} + +// SetFilename formats the attachment's filename. +func (h *AttachmentHeader) SetFilename(filename string) { + dispParams := map[string]string{"filename": filename} + h.SetContentDisposition("attachment", dispParams) +} diff --git a/vendor/github.com/emersion/go-message/mail/header.go b/vendor/github.com/emersion/go-message/mail/header.go new file mode 100644 index 000000000..4dd89ef65 --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/header.go @@ -0,0 +1,381 @@ +package mail + +import ( + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "net/mail" + "os" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/emersion/go-message" +) + +const dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700" + +type headerParser struct { + s string +} + +func (p *headerParser) len() int { + return len(p.s) +} + +func (p *headerParser) empty() bool { + return p.len() == 0 +} + +func (p *headerParser) peek() byte { + return p.s[0] +} + +func (p *headerParser) consume(c byte) bool { + if p.empty() || p.peek() != c { + return false + } + p.s = p.s[1:] + return true +} + +// skipSpace skips the leading space and tab characters. +func (p *headerParser) skipSpace() { + p.s = strings.TrimLeft(p.s, " \t") +} + +// skipCFWS skips CFWS as defined in RFC5322. It returns false if the CFWS is +// malformed. +func (p *headerParser) skipCFWS() bool { + p.skipSpace() + + for { + if !p.consume('(') { + break + } + + if _, ok := p.consumeComment(); !ok { + return false + } + + p.skipSpace() + } + + return true +} + +func (p *headerParser) consumeComment() (string, bool) { + // '(' already consumed. + depth := 1 + + var comment string + for { + if p.empty() || depth == 0 { + break + } + + if p.peek() == '\\' && p.len() > 1 { + p.s = p.s[1:] + } else if p.peek() == '(' { + depth++ + } else if p.peek() == ')' { + depth-- + } + + if depth > 0 { + comment += p.s[:1] + } + + p.s = p.s[1:] + } + + return comment, depth == 0 +} + +func (p *headerParser) parseAtomText(dot bool) (string, error) { + i := 0 + for { + r, size := utf8.DecodeRuneInString(p.s[i:]) + if size == 1 && r == utf8.RuneError { + return "", fmt.Errorf("mail: invalid UTF-8 in atom-text: %q", p.s) + } else if size == 0 || !isAtext(r, dot) { + break + } + i += size + } + if i == 0 { + return "", errors.New("mail: invalid string") + } + + var atom string + atom, p.s = p.s[:i], p.s[i:] + return atom, nil +} + +func isAtext(r rune, dot bool) bool { + switch r { + case '.': + return dot + // RFC 5322 3.2.3 specials + case '(', ')', '[', ']', ';', '@', '\\', ',': + return false + case '<', '>', '"', ':': + return false + } + return isVchar(r) +} + +// isVchar reports whether r is an RFC 5322 VCHAR character. +func isVchar(r rune) bool { + // Visible (printing) characters + return '!' <= r && r <= '~' || isMultibyte(r) +} + +// isMultibyte reports whether r is a multi-byte UTF-8 character +// as supported by RFC 6532 +func isMultibyte(r rune) bool { + return r >= utf8.RuneSelf +} + +func (p *headerParser) parseNoFoldLiteral() (string, error) { + if !p.consume('[') { + return "", errors.New("mail: missing '[' in no-fold-literal") + } + + i := 0 + for { + r, size := utf8.DecodeRuneInString(p.s[i:]) + if size == 1 && r == utf8.RuneError { + return "", fmt.Errorf("mail: invalid UTF-8 in no-fold-literal: %q", p.s) + } else if size == 0 || !isDtext(r) { + break + } + i += size + } + var lit string + lit, p.s = p.s[:i], p.s[i:] + + if !p.consume(']') { + return "", errors.New("mail: missing ']' in no-fold-literal") + } + return "[" + lit + "]", nil +} + +func isDtext(r rune) bool { + switch r { + case '[', ']', '\\': + return false + } + return isVchar(r) +} + +func (p *headerParser) parseMsgID() (string, error) { + if !p.skipCFWS() { + return "", errors.New("mail: malformed parenthetical comment") + } + + if !p.consume('<') { + return "", errors.New("mail: missing '<' in msg-id") + } + + left, err := p.parseAtomText(true) + if err != nil { + return "", err + } + + if !p.consume('@') { + return "", errors.New("mail: missing '@' in msg-id") + } + + var right string + if !p.empty() && p.peek() == '[' { + // no-fold-literal + right, err = p.parseNoFoldLiteral() + } else { + right, err = p.parseAtomText(true) + } + if err != nil { + return "", err + } + + if !p.consume('>') { + return "", errors.New("mail: missing '>' in msg-id") + } + + if !p.skipCFWS() { + return "", errors.New("mail: malformed parenthetical comment") + } + + return left + "@" + right, nil +} + +// A Header is a mail header. +type Header struct { + message.Header +} + +// HeaderFromMap creates a header from a map of header fields. +// +// This function is provided for interoperability with the standard library. +// If possible, ReadHeader should be used instead to avoid loosing information. +// The map representation looses the ordering of the fields, the capitalization +// of the header keys, and the whitespace of the original header. +func HeaderFromMap(m map[string][]string) Header { + return Header{message.HeaderFromMap(m)} +} + +// AddressList parses the named header field as a list of addresses. If the +// header field is missing, it returns nil. +// +// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields. +func (h *Header) AddressList(key string) ([]*Address, error) { + v := h.Get(key) + if v == "" { + return nil, nil + } + return ParseAddressList(v) +} + +// SetAddressList formats the named header field to the provided list of +// addresses. +// +// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields. +func (h *Header) SetAddressList(key string, addrs []*Address) { + if len(addrs) > 0 { + h.Set(key, formatAddressList(addrs)) + } else { + h.Del(key) + } +} + +// Date parses the Date header field. If the header field is missing, it +// returns the zero time. +func (h *Header) Date() (time.Time, error) { + v := h.Get("Date") + if v == "" { + return time.Time{}, nil + } + return mail.ParseDate(v) +} + +// SetDate formats the Date header field. +func (h *Header) SetDate(t time.Time) { + if !t.IsZero() { + h.Set("Date", t.Format(dateLayout)) + } else { + h.Del("Date") + } +} + +// Subject parses the Subject header field. If there is an error, the raw field +// value is returned alongside the error. +func (h *Header) Subject() (string, error) { + return h.Text("Subject") +} + +// SetSubject formats the Subject header field. +func (h *Header) SetSubject(s string) { + h.SetText("Subject", s) +} + +// MessageID parses the Message-ID field. It returns the message identifier, +// without the angle brackets. If the message doesn't have a Message-ID header +// field, it returns an empty string. +func (h *Header) MessageID() (string, error) { + v := h.Get("Message-Id") + if v == "" { + return "", nil + } + + p := headerParser{v} + return p.parseMsgID() +} + +// MsgIDList parses a list of message identifiers. It returns message +// identifiers without angle brackets. If the header field is missing, it +// returns nil. +// +// This can be used on In-Reply-To and References header fields. +func (h *Header) MsgIDList(key string) ([]string, error) { + v := h.Get(key) + if v == "" { + return nil, nil + } + + p := headerParser{v} + var l []string + for !p.empty() { + msgID, err := p.parseMsgID() + if err != nil { + return l, err + } + l = append(l, msgID) + } + + return l, nil +} + +// GenerateMessageID wraps GenerateMessageIDWithHostname and therefore uses the +// hostname of the local machine. This is done to not break existing software. +// Wherever possible better use GenerateMessageIDWithHostname, because the local +// hostname of a machine tends to not be unique nor a FQDN which especially +// brings problems with spam filters. +func (h *Header) GenerateMessageID() error { + var err error + hostname, err := os.Hostname() + if err != nil { + return err + } + return h.GenerateMessageIDWithHostname(hostname) +} + +// GenerateMessageIDWithHostname generates an RFC 2822-compliant Message-Id +// based on the informational draft "Recommendations for generating Message +// IDs", it takes an hostname as argument, so that software using this library +// could use a hostname they know to be unique +func (h *Header) GenerateMessageIDWithHostname(hostname string) error { + now := uint64(time.Now().UnixNano()) + + nonceByte := make([]byte, 8) + if _, err := rand.Read(nonceByte); err != nil { + return err + } + nonce := binary.BigEndian.Uint64(nonceByte) + + msgID := fmt.Sprintf("%s.%s@%s", base36(now), base36(nonce), hostname) + h.SetMessageID(msgID) + return nil +} + +func base36(input uint64) string { + return strings.ToUpper(strconv.FormatUint(input, 36)) +} + +// SetMessageID sets the Message-ID field. id is the message identifier, +// without the angle brackets. +func (h *Header) SetMessageID(id string) { + if id != "" { + h.Set("Message-Id", "<"+id+">") + } else { + h.Del("Message-Id") + } +} + +// SetMsgIDList formats a list of message identifiers. Message identifiers +// don't include angle brackets. +// +// This can be used on In-Reply-To and References header fields. +func (h *Header) SetMsgIDList(key string, l []string) { + if len(l) > 0 { + h.Set(key, "<"+strings.Join(l, "> <")+">") + } else { + h.Del(key) + } +} + +// Copy creates a stand-alone copy of the header. +func (h *Header) Copy() Header { + return Header{h.Header.Copy()} +} diff --git a/vendor/github.com/emersion/go-message/mail/inline.go b/vendor/github.com/emersion/go-message/mail/inline.go new file mode 100644 index 000000000..2aadfdcae --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/inline.go @@ -0,0 +1,10 @@ +package mail + +import ( + "github.com/emersion/go-message" +) + +// A InlineHeader represents a message text header. +type InlineHeader struct { + message.Header +} diff --git a/vendor/github.com/emersion/go-message/mail/mail.go b/vendor/github.com/emersion/go-message/mail/mail.go new file mode 100644 index 000000000..2f9a12c91 --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/mail.go @@ -0,0 +1,9 @@ +// Package mail implements reading and writing mail messages. +// +// This package assumes that a mail message contains one or more text parts and +// zero or more attachment parts. Each text part represents a different version +// of the message content (e.g. a different type, a different language and so +// on). +// +// RFC 5322 defines the Internet Message Format. +package mail diff --git a/vendor/github.com/emersion/go-message/mail/reader.go b/vendor/github.com/emersion/go-message/mail/reader.go new file mode 100644 index 000000000..f721a452b --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/reader.go @@ -0,0 +1,130 @@ +package mail + +import ( + "container/list" + "io" + "strings" + + "github.com/emersion/go-message" +) + +// A PartHeader is a mail part header. It contains convenience functions to get +// and set header fields. +type PartHeader interface { + // Add adds the key, value pair to the header. + Add(key, value string) + // Del deletes the values associated with key. + Del(key string) + // Get gets the first value associated with the given key. If there are no + // values associated with the key, Get returns "". + Get(key string) string + // Set sets the header entries associated with key to the single element + // value. It replaces any existing values associated with key. + Set(key, value string) +} + +// A Part is either a mail text or an attachment. Header is either a InlineHeader +// or an AttachmentHeader. +type Part struct { + Header PartHeader + Body io.Reader +} + +// A Reader reads a mail message. +type Reader struct { + Header Header + + e *message.Entity + readers *list.List +} + +// NewReader creates a new mail reader. +func NewReader(e *message.Entity) *Reader { + mr := e.MultipartReader() + if mr == nil { + // Artificially create a multipart entity + // With this header, no error will be returned by message.NewMultipart + var h message.Header + h.Set("Content-Type", "multipart/mixed") + me, _ := message.NewMultipart(h, []*message.Entity{e}) + mr = me.MultipartReader() + } + + l := list.New() + l.PushBack(mr) + + return &Reader{Header{e.Header}, e, l} +} + +// CreateReader reads a mail header from r and returns a new mail reader. +// +// If the message uses an unknown transfer encoding or charset, CreateReader +// returns an error that verifies message.IsUnknownCharset, but also returns a +// Reader that can be used. +func CreateReader(r io.Reader) (*Reader, error) { + e, err := message.Read(r) + if err != nil && !message.IsUnknownCharset(err) { + return nil, err + } + + return NewReader(e), err +} + +// NextPart returns the next mail part. If there is no more part, io.EOF is +// returned as error. +// +// The returned Part.Body must be read completely before the next call to +// NextPart, otherwise it will be discarded. +// +// If the part uses an unknown transfer encoding or charset, NextPart returns an +// error that verifies message.IsUnknownCharset, but also returns a Part that +// can be used. +func (r *Reader) NextPart() (*Part, error) { + for r.readers.Len() > 0 { + e := r.readers.Back() + mr := e.Value.(message.MultipartReader) + + p, err := mr.NextPart() + if err == io.EOF { + // This whole multipart entity has been read, continue with the next one + r.readers.Remove(e) + continue + } else if err != nil && !message.IsUnknownCharset(err) { + return nil, err + } + + if pmr := p.MultipartReader(); pmr != nil { + // This is a multipart part, read it + r.readers.PushBack(pmr) + } else { + // This is a non-multipart part, return a mail part + mp := &Part{Body: p.Body} + t, _, _ := p.Header.ContentType() + disp, _, _ := p.Header.ContentDisposition() + if disp == "inline" || (disp != "attachment" && strings.HasPrefix(t, "text/")) { + mp.Header = &InlineHeader{p.Header} + } else { + mp.Header = &AttachmentHeader{p.Header} + } + return mp, err + } + } + + return nil, io.EOF +} + +// Close finishes the reader. +func (r *Reader) Close() error { + for r.readers.Len() > 0 { + e := r.readers.Back() + mr := e.Value.(message.MultipartReader) + + if err := mr.Close(); err != nil { + return err + } + + r.readers.Remove(e) + } + + return nil +} diff --git a/vendor/github.com/emersion/go-message/mail/writer.go b/vendor/github.com/emersion/go-message/mail/writer.go new file mode 100644 index 000000000..6e6a0d24b --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/writer.go @@ -0,0 +1,132 @@ +package mail + +import ( + "io" + "strings" + + "github.com/emersion/go-message" +) + +func initInlineContentTransferEncoding(h *message.Header) { + if !h.Has("Content-Transfer-Encoding") { + t, _, _ := h.ContentType() + if strings.HasPrefix(t, "text/") { + h.Set("Content-Transfer-Encoding", "quoted-printable") + } else { + h.Set("Content-Transfer-Encoding", "base64") + } + } +} + +func initInlineHeader(h *InlineHeader) { + h.Set("Content-Disposition", "inline") + initInlineContentTransferEncoding(&h.Header) +} + +func initAttachmentHeader(h *AttachmentHeader) { + disp, _, _ := h.ContentDisposition() + if disp != "attachment" { + h.Set("Content-Disposition", "attachment") + } + if !h.Has("Content-Transfer-Encoding") { + h.Set("Content-Transfer-Encoding", "base64") + } +} + +// A Writer writes a mail message. A mail message contains one or more text +// parts and zero or more attachments. +type Writer struct { + mw *message.Writer +} + +// CreateWriter writes a mail header to w and creates a new Writer. +func CreateWriter(w io.Writer, header Header) (*Writer, error) { + header = header.Copy() // don't modify the caller's view + header.Set("Content-Type", "multipart/mixed") + + mw, err := message.CreateWriter(w, header.Header) + if err != nil { + return nil, err + } + + return &Writer{mw}, nil +} + +// CreateInlineWriter writes a mail header to w. The mail will contain an +// inline part, allowing to represent the same text in different formats. +// Attachments cannot be included. +func CreateInlineWriter(w io.Writer, header Header) (*InlineWriter, error) { + header = header.Copy() // don't modify the caller's view + header.Set("Content-Type", "multipart/alternative") + + mw, err := message.CreateWriter(w, header.Header) + if err != nil { + return nil, err + } + + return &InlineWriter{mw}, nil +} + +// CreateSingleInlineWriter writes a mail header to w. The mail will contain a +// single inline part. The body of the part should be written to the returned +// io.WriteCloser. Only one single inline part should be written, use +// CreateWriter if you want multiple parts. +func CreateSingleInlineWriter(w io.Writer, header Header) (io.WriteCloser, error) { + header = header.Copy() // don't modify the caller's view + initInlineContentTransferEncoding(&header.Header) + return message.CreateWriter(w, header.Header) +} + +// CreateInline creates a InlineWriter. One or more parts representing the same +// text in different formats can be written to a InlineWriter. +func (w *Writer) CreateInline() (*InlineWriter, error) { + var h message.Header + h.Set("Content-Type", "multipart/alternative") + + mw, err := w.mw.CreatePart(h) + if err != nil { + return nil, err + } + return &InlineWriter{mw}, nil +} + +// CreateSingleInline creates a new single text part with the provided header. +// The body of the part should be written to the returned io.WriteCloser. Only +// one single text part should be written, use CreateInline if you want multiple +// text parts. +func (w *Writer) CreateSingleInline(h InlineHeader) (io.WriteCloser, error) { + h = InlineHeader{h.Header.Copy()} // don't modify the caller's view + initInlineHeader(&h) + return w.mw.CreatePart(h.Header) +} + +// CreateAttachment creates a new attachment with the provided header. The body +// of the part should be written to the returned io.WriteCloser. +func (w *Writer) CreateAttachment(h AttachmentHeader) (io.WriteCloser, error) { + h = AttachmentHeader{h.Header.Copy()} // don't modify the caller's view + initAttachmentHeader(&h) + return w.mw.CreatePart(h.Header) +} + +// Close finishes the Writer. +func (w *Writer) Close() error { + return w.mw.Close() +} + +// InlineWriter writes a mail message's text. +type InlineWriter struct { + mw *message.Writer +} + +// CreatePart creates a new text part with the provided header. The body of the +// part should be written to the returned io.WriteCloser. +func (w *InlineWriter) CreatePart(h InlineHeader) (io.WriteCloser, error) { + h = InlineHeader{h.Header.Copy()} // don't modify the caller's view + initInlineHeader(&h) + return w.mw.CreatePart(h.Header) +} + +// Close finishes the InlineWriter. +func (w *InlineWriter) Close() error { + return w.mw.Close() +} diff --git a/vendor/github.com/emersion/go-message/message.go b/vendor/github.com/emersion/go-message/message.go new file mode 100644 index 000000000..52cf115a5 --- /dev/null +++ b/vendor/github.com/emersion/go-message/message.go @@ -0,0 +1,15 @@ +// Package message implements reading and writing multipurpose messages. +// +// RFC 2045, RFC 2046 and RFC 2047 defines MIME, and RFC 2183 defines the +// Content-Disposition header field. +// +// Add this import to your package if you want to handle most common charsets +// by default: +// +// import ( +// _ "github.com/emersion/go-message/charset" +// ) +// +// Note, non-UTF-8 charsets are only supported when reading messages. Only +// UTF-8 is supported when writing messages. +package message diff --git a/vendor/github.com/emersion/go-message/multipart.go b/vendor/github.com/emersion/go-message/multipart.go new file mode 100644 index 000000000..c406a3113 --- /dev/null +++ b/vendor/github.com/emersion/go-message/multipart.go @@ -0,0 +1,116 @@ +package message + +import ( + "io" + + "github.com/emersion/go-message/textproto" +) + +// MultipartReader is an iterator over parts in a MIME multipart body. +type MultipartReader interface { + io.Closer + + // NextPart returns the next part in the multipart or an error. When there are + // no more parts, the error io.EOF is returned. + // + // Entity.Body must be read completely before the next call to NextPart, + // otherwise it will be discarded. + NextPart() (*Entity, error) +} + +type multipartReader struct { + r *textproto.MultipartReader +} + +// NextPart implements MultipartReader. +func (r *multipartReader) NextPart() (*Entity, error) { + p, err := r.r.NextPart() + if err != nil { + return nil, err + } + return New(Header{p.Header}, p) +} + +// Close implements io.Closer. +func (r *multipartReader) Close() error { + return nil +} + +type multipartBody struct { + header Header + parts []*Entity + + r *io.PipeReader + w *Writer + + i int +} + +// Read implements io.Reader. +func (m *multipartBody) Read(p []byte) (n int, err error) { + if m.r == nil { + r, w := io.Pipe() + m.r = r + + var err error + m.w, err = createWriter(w, &m.header) + if err != nil { + return 0, err + } + + // Prevent calls to NextPart to succeed + m.i = len(m.parts) + + go func() { + if err := m.writeBodyTo(m.w); err != nil { + w.CloseWithError(err) + return + } + + if err := m.w.Close(); err != nil { + w.CloseWithError(err) + return + } + + w.Close() + }() + } + + return m.r.Read(p) +} + +// Close implements io.Closer. +func (m *multipartBody) Close() error { + if m.r != nil { + m.r.Close() + } + return nil +} + +// NextPart implements MultipartReader. +func (m *multipartBody) NextPart() (*Entity, error) { + if m.i >= len(m.parts) { + return nil, io.EOF + } + + part := m.parts[m.i] + m.i++ + return part, nil +} + +func (m *multipartBody) writeBodyTo(w *Writer) error { + for _, p := range m.parts { + pw, err := w.CreatePart(p.Header) + if err != nil { + return err + } + + if err := p.writeBodyTo(pw); err != nil { + return err + } + if err := pw.Close(); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/emersion/go-message/textproto/header.go b/vendor/github.com/emersion/go-message/textproto/header.go new file mode 100644 index 000000000..10c04f319 --- /dev/null +++ b/vendor/github.com/emersion/go-message/textproto/header.go @@ -0,0 +1,677 @@ +package textproto + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/textproto" + "sort" + "strings" +) + +type headerField struct { + b []byte // Raw header field, including whitespace + + k string + v string +} + +func newHeaderField(k, v string, b []byte) *headerField { + return &headerField{k: textproto.CanonicalMIMEHeaderKey(k), v: v, b: b} +} + +func (f *headerField) raw() ([]byte, error) { + if f.b != nil { + return f.b, nil + } else { + for pos, ch := range f.k { + // check if character is a printable US-ASCII except ':' + if !(ch >= '!' && ch < ':' || ch > ':' && ch <= '~') { + return nil, fmt.Errorf("field name contains incorrect symbols (\\x%x at %v)", ch, pos) + } + } + + if pos := strings.IndexAny(f.v, "\r\n"); pos != -1 { + return nil, fmt.Errorf("field value contains \\r\\n (at %v)", pos) + } + + return []byte(formatHeaderField(f.k, f.v)), nil + } +} + +// A Header represents the key-value pairs in a message header. +// +// The header representation is idempotent: if the header can be read and +// written, the result will be exactly the same as the original (including +// whitespace and header field ordering). This is required for e.g. DKIM. +// +// Mutating the header is restricted: the only two allowed operations are +// inserting a new header field at the top and deleting a header field. This is +// again necessary for DKIM. +type Header struct { + // Fields are in reverse order so that inserting a new field at the top is + // cheap. + l []*headerField + m map[string][]*headerField +} + +func makeHeaderMap(fs []*headerField) map[string][]*headerField { + if len(fs) == 0 { + return nil + } + + m := make(map[string][]*headerField, len(fs)) + for i, f := range fs { + m[f.k] = append(m[f.k], fs[i]) + } + return m +} + +func newHeader(fs []*headerField) Header { + // Reverse order + for i := len(fs)/2 - 1; i >= 0; i-- { + opp := len(fs) - 1 - i + fs[i], fs[opp] = fs[opp], fs[i] + } + + return Header{l: fs, m: makeHeaderMap(fs)} +} + +// HeaderFromMap creates a header from a map of header fields. +// +// This function is provided for interoperability with the standard library. +// If possible, ReadHeader should be used instead to avoid loosing information. +// The map representation looses the ordering of the fields, the capitalization +// of the header keys, and the whitespace of the original header. +func HeaderFromMap(m map[string][]string) Header { + fs := make([]*headerField, 0, len(m)) + for k, vs := range m { + for _, v := range vs { + fs = append(fs, newHeaderField(k, v, nil)) + } + } + + sort.SliceStable(fs, func(i, j int) bool { + return fs[i].k < fs[j].k + }) + + return newHeader(fs) +} + +// AddRaw adds the raw key, value pair to the header. +// +// The supplied byte slice should be a complete field in the "Key: Value" form +// including trailing CRLF. If there is no comma in the input - AddRaw panics. +// No changes are made to kv contents and it will be copied into WriteHeader +// output as is. +// +// kv is directly added to the underlying structure and therefore should not be +// modified after the AddRaw call. +func (h *Header) AddRaw(kv []byte) { + colon := bytes.IndexByte(kv, ':') + if colon == -1 { + panic("textproto: Header.AddRaw: missing colon") + } + k := textproto.CanonicalMIMEHeaderKey(string(trim(kv[:colon]))) + v := trimAroundNewlines(kv[colon+1:]) + + if h.m == nil { + h.m = make(map[string][]*headerField) + } + + f := newHeaderField(k, v, kv) + h.l = append(h.l, f) + h.m[k] = append(h.m[k], f) +} + +// Add adds the key, value pair to the header. It prepends to any existing +// fields associated with key. +// +// Key and value should obey character requirements of RFC 6532. +// If you need to format or fold lines manually, use AddRaw. +func (h *Header) Add(k, v string) { + k = textproto.CanonicalMIMEHeaderKey(k) + + if h.m == nil { + h.m = make(map[string][]*headerField) + } + + f := newHeaderField(k, v, nil) + h.l = append(h.l, f) + h.m[k] = append(h.m[k], f) +} + +// Get gets the first value associated with the given key. If there are no +// values associated with the key, Get returns "". +func (h *Header) Get(k string) string { + fields := h.m[textproto.CanonicalMIMEHeaderKey(k)] + if len(fields) == 0 { + return "" + } + return fields[len(fields)-1].v +} + +// Raw gets the first raw header field associated with the given key. +// +// The returned bytes contain a complete field in the "Key: value" form, +// including trailing CRLF. +// +// The returned slice should not be modified and becomes invalid when the +// header is updated. +// +// An error is returned if the header field contains incorrect characters (see +// RFC 6532). +func (h *Header) Raw(k string) ([]byte, error) { + fields := h.m[textproto.CanonicalMIMEHeaderKey(k)] + if len(fields) == 0 { + return nil, nil + } + return fields[len(fields)-1].raw() +} + +// Values returns all values associated with the given key. +// +// The returned slice should not be modified and becomes invalid when the +// header is updated. +func (h *Header) Values(k string) []string { + fields := h.m[textproto.CanonicalMIMEHeaderKey(k)] + if len(fields) == 0 { + return nil + } + l := make([]string, len(fields)) + for i, field := range fields { + l[len(fields)-i-1] = field.v + } + return l +} + +// Set sets the header fields associated with key to the single field value. +// It replaces any existing values associated with key. +func (h *Header) Set(k, v string) { + h.Del(k) + h.Add(k, v) +} + +// Del deletes the values associated with key. +func (h *Header) Del(k string) { + k = textproto.CanonicalMIMEHeaderKey(k) + + delete(h.m, k) + + // Delete existing keys + for i := len(h.l) - 1; i >= 0; i-- { + if h.l[i].k == k { + h.l = append(h.l[:i], h.l[i+1:]...) + } + } +} + +// Has checks whether the header has a field with the specified key. +func (h *Header) Has(k string) bool { + _, ok := h.m[textproto.CanonicalMIMEHeaderKey(k)] + return ok +} + +// Copy creates an independent copy of the header. +func (h *Header) Copy() Header { + l := make([]*headerField, len(h.l)) + copy(l, h.l) + m := makeHeaderMap(l) + return Header{l: l, m: m} +} + +// Len returns the number of fields in the header. +func (h *Header) Len() int { + return len(h.l) +} + +// Map returns all header fields as a map. +// +// This function is provided for interoperability with the standard library. +// If possible, Fields should be used instead to avoid loosing information. +// The map representation looses the ordering of the fields, the capitalization +// of the header keys, and the whitespace of the original header. +func (h *Header) Map() map[string][]string { + m := make(map[string][]string, h.Len()) + fields := h.Fields() + for fields.Next() { + m[fields.Key()] = append(m[fields.Key()], fields.Value()) + } + return m +} + +// HeaderFields iterates over header fields. Its cursor starts before the first +// field of the header. Use Next to advance from field to field. +type HeaderFields interface { + // Next advances to the next header field. It returns true on success, or + // false if there is no next field. + Next() (more bool) + // Key returns the key of the current field. + Key() string + // Value returns the value of the current field. + Value() string + // Raw returns the raw current header field. See Header.Raw. + Raw() ([]byte, error) + // Del deletes the current field. + Del() + // Len returns the amount of header fields in the subset of header iterated + // by this HeaderFields instance. + // + // For Fields(), it will return the amount of fields in the whole header section. + // For FieldsByKey(), it will return the amount of fields with certain key. + Len() int +} + +type headerFields struct { + h *Header + cur int +} + +func (fs *headerFields) Next() bool { + fs.cur++ + return fs.cur < len(fs.h.l) +} + +func (fs *headerFields) index() int { + if fs.cur < 0 { + panic("message: HeaderFields method called before Next") + } + if fs.cur >= len(fs.h.l) { + panic("message: HeaderFields method called after Next returned false") + } + return len(fs.h.l) - fs.cur - 1 +} + +func (fs *headerFields) field() *headerField { + return fs.h.l[fs.index()] +} + +func (fs *headerFields) Key() string { + return fs.field().k +} + +func (fs *headerFields) Value() string { + return fs.field().v +} + +func (fs *headerFields) Raw() ([]byte, error) { + return fs.field().raw() +} + +func (fs *headerFields) Del() { + f := fs.field() + + ok := false + for i, ff := range fs.h.m[f.k] { + if ff == f { + ok = true + fs.h.m[f.k] = append(fs.h.m[f.k][:i], fs.h.m[f.k][i+1:]...) + if len(fs.h.m[f.k]) == 0 { + delete(fs.h.m, f.k) + } + break + } + } + if !ok { + panic("message: field not found in Header.m") + } + + fs.h.l = append(fs.h.l[:fs.index()], fs.h.l[fs.index()+1:]...) + fs.cur-- +} + +func (fs *headerFields) Len() int { + return len(fs.h.l) +} + +// Fields iterates over all the header fields. +// +// The header may not be mutated while iterating, except using HeaderFields.Del. +func (h *Header) Fields() HeaderFields { + return &headerFields{h, -1} +} + +type headerFieldsByKey struct { + h *Header + k string + cur int +} + +func (fs *headerFieldsByKey) Next() bool { + fs.cur++ + return fs.cur < len(fs.h.m[fs.k]) +} + +func (fs *headerFieldsByKey) index() int { + if fs.cur < 0 { + panic("message: headerfields.key or value called before next") + } + if fs.cur >= len(fs.h.m[fs.k]) { + panic("message: headerfields.key or value called after next returned false") + } + return len(fs.h.m[fs.k]) - fs.cur - 1 +} + +func (fs *headerFieldsByKey) field() *headerField { + return fs.h.m[fs.k][fs.index()] +} + +func (fs *headerFieldsByKey) Key() string { + return fs.field().k +} + +func (fs *headerFieldsByKey) Value() string { + return fs.field().v +} + +func (fs *headerFieldsByKey) Raw() ([]byte, error) { + return fs.field().raw() +} + +func (fs *headerFieldsByKey) Del() { + f := fs.field() + + ok := false + for i := range fs.h.l { + if f == fs.h.l[i] { + ok = true + fs.h.l = append(fs.h.l[:i], fs.h.l[i+1:]...) + break + } + } + if !ok { + panic("message: field not found in Header.l") + } + + fs.h.m[fs.k] = append(fs.h.m[fs.k][:fs.index()], fs.h.m[fs.k][fs.index()+1:]...) + if len(fs.h.m[fs.k]) == 0 { + delete(fs.h.m, fs.k) + } + fs.cur-- +} + +func (fs *headerFieldsByKey) Len() int { + return len(fs.h.m[fs.k]) +} + +// FieldsByKey iterates over all fields having the specified key. +// +// The header may not be mutated while iterating, except using HeaderFields.Del. +func (h *Header) FieldsByKey(k string) HeaderFields { + return &headerFieldsByKey{h, textproto.CanonicalMIMEHeaderKey(k), -1} +} + +func readLineSlice(r *bufio.Reader, line []byte) ([]byte, error) { + for { + l, more, err := r.ReadLine() + line = append(line, l...) + if err != nil { + return line, err + } + + if !more { + break + } + } + + return line, nil +} + +func isSpace(c byte) bool { + return c == ' ' || c == '\t' +} + +func validHeaderKeyByte(b byte) bool { + c := int(b) + return c >= 33 && c <= 126 && c != ':' +} + +// trim returns s with leading and trailing spaces and tabs removed. +// It does not assume Unicode or UTF-8. +func trim(s []byte) []byte { + i := 0 + for i < len(s) && isSpace(s[i]) { + i++ + } + n := len(s) + for n > i && isSpace(s[n-1]) { + n-- + } + return s[i:n] +} + +func hasContinuationLine(r *bufio.Reader) bool { + c, err := r.ReadByte() + if err != nil { + return false // bufio will keep err until next read. + } + r.UnreadByte() + return isSpace(c) +} + +func readContinuedLineSlice(r *bufio.Reader) ([]byte, error) { + // Read the first line. We preallocate slice that it enough + // for most fields. + line, err := readLineSlice(r, make([]byte, 0, 256)) + if err == io.EOF && len(line) == 0 { + // Header without a body + return nil, nil + } else if err != nil { + return nil, err + } + + if len(line) == 0 { // blank line - no continuation + return line, nil + } + + line = append(line, '\r', '\n') + + // Read continuation lines. + for hasContinuationLine(r) { + line, err = readLineSlice(r, line) + if err != nil { + break // bufio will keep err until next read. + } + + line = append(line, '\r', '\n') + } + + return line, nil +} + +func writeContinued(b *strings.Builder, l []byte) { + // Strip trailing \r, if any + if len(l) > 0 && l[len(l)-1] == '\r' { + l = l[:len(l)-1] + } + l = trim(l) + if len(l) == 0 { + return + } + if b.Len() > 0 { + b.WriteByte(' ') + } + b.Write(l) +} + +// Strip newlines and spaces around newlines. +func trimAroundNewlines(v []byte) string { + var b strings.Builder + b.Grow(len(v)) + for { + i := bytes.IndexByte(v, '\n') + if i < 0 { + writeContinued(&b, v) + break + } + writeContinued(&b, v[:i]) + v = v[i+1:] + } + + return b.String() +} + +// ReadHeader reads a MIME header from r. The header is a sequence of possibly +// continued "Key: Value" lines ending in a blank line. +// +// To avoid denial of service attacks, the provided bufio.Reader should be +// reading from an io.LimitedReader or a similar Reader to bound the size of +// headers. +func ReadHeader(r *bufio.Reader) (Header, error) { + fs := make([]*headerField, 0, 32) + + // The first line cannot start with a leading space. + if buf, err := r.Peek(1); err == nil && isSpace(buf[0]) { + line, err := readLineSlice(r, nil) + if err != nil { + return newHeader(fs), err + } + + return newHeader(fs), fmt.Errorf("message: malformed MIME header initial line: %v", string(line)) + } + + for { + kv, err := readContinuedLineSlice(r) + if len(kv) == 0 { + return newHeader(fs), err + } + + // Key ends at first colon; should not have trailing spaces but they + // appear in the wild, violating specs, so we remove them if present. + i := bytes.IndexByte(kv, ':') + if i < 0 { + return newHeader(fs), fmt.Errorf("message: malformed MIME header line: %v", string(kv)) + } + + keyBytes := trim(kv[:i]) + + // Verify that there are no invalid characters in the header key. + // See RFC 5322 Section 2.2 + for _, c := range keyBytes { + if !validHeaderKeyByte(c) { + return newHeader(fs), fmt.Errorf("message: malformed MIME header key: %v", string(keyBytes)) + } + } + + key := textproto.CanonicalMIMEHeaderKey(string(keyBytes)) + + // As per RFC 7230 field-name is a token, tokens consist of one or more + // chars. We could return a an error here, but better to be liberal in + // what we accept, so if we get an empty key, skip it. + if key == "" { + continue + } + + i++ // skip colon + v := kv[i:] + + value := trimAroundNewlines(v) + fs = append(fs, newHeaderField(key, value, kv)) + + if err != nil { + return newHeader(fs), err + } + } +} + +func foldLine(v string, maxlen int) (line, next string, ok bool) { + ok = true + + // We'll need to fold before maxlen + foldBefore := maxlen + 1 + foldAt := len(v) + + var folding string + if foldBefore > len(v) { + // We reached the end of the string + if v[len(v)-1] != '\n' { + // If there isn't already a trailing CRLF, insert one + folding = "\r\n" + } + } else { + // Find the closest whitespace before maxlen + foldAt = strings.LastIndexAny(v[:foldBefore], " \t\n") + + if foldAt == 0 { + // The whitespace we found was the previous folding WSP + foldAt = foldBefore - 1 + } else if foldAt < 0 { + // We didn't find any whitespace, we have to insert one + foldAt = foldBefore - 2 + } + + switch v[foldAt] { + case ' ', '\t': + if v[foldAt-1] != '\n' { + folding = "\r\n" // The next char will be a WSP, don't need to insert one + } + case '\n': + folding = "" // There is already a CRLF, nothing to do + default: + // Another char, we need to insert CRLF + WSP. This will insert an + // extra space in the string, so this should be avoided if + // possible. + folding = "\r\n " + ok = false + } + } + + return v[:foldAt] + folding, v[foldAt:], ok +} + +const ( + preferredHeaderLen = 76 + maxHeaderLen = 998 +) + +// formatHeaderField formats a header field, ensuring each line is no longer +// than 76 characters. It tries to fold lines at whitespace characters if +// possible. If the header contains a word longer than this limit, it will be +// split. +func formatHeaderField(k, v string) string { + s := k + ": " + + if v == "" { + return s + "\r\n" + } + + first := true + for len(v) > 0 { + // If this is the first line, substract the length of the key + keylen := 0 + if first { + keylen = len(s) + } + + // First try with a soft limit + l, next, ok := foldLine(v, preferredHeaderLen-keylen) + if !ok { + // Folding failed to preserve the original header field value. Try + // with a larger, hard limit. + l, next, _ = foldLine(v, maxHeaderLen-keylen) + } + v = next + s += l + first = false + } + + return s +} + +// WriteHeader writes a MIME header to w. +func WriteHeader(w io.Writer, h Header) error { + for i := len(h.l) - 1; i >= 0; i-- { + f := h.l[i] + if rawField, err := f.raw(); err == nil { + if _, err := w.Write(rawField); err != nil { + return err + } + } else { + return fmt.Errorf("failed to write header field #%v (%q): %w", len(h.l)-i, f.k, err) + } + } + + _, err := w.Write([]byte{'\r', '\n'}) + return err +} diff --git a/vendor/github.com/emersion/go-message/textproto/multipart.go b/vendor/github.com/emersion/go-message/textproto/multipart.go new file mode 100644 index 000000000..62824dfdd --- /dev/null +++ b/vendor/github.com/emersion/go-message/textproto/multipart.go @@ -0,0 +1,474 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package textproto + +// Multipart is defined in RFC 2046. + +import ( + "bufio" + "bytes" + "crypto/rand" + "errors" + "fmt" + "io" + "io/ioutil" +) + +var emptyParams = make(map[string]string) + +// This constant needs to be at least 76 for this package to work correctly. +// This is because \r\n--separator_of_len_70- would fill the buffer and it +// wouldn't be safe to consume a single byte from it. +const peekBufferSize = 4096 + +// A Part represents a single part in a multipart body. +type Part struct { + Header Header + + mr *MultipartReader + + // r is either a reader directly reading from mr + r io.Reader + + n int // known data bytes waiting in mr.bufReader + total int64 // total data bytes read already + err error // error to return when n == 0 + readErr error // read error observed from mr.bufReader +} + +// NewMultipartReader creates a new multipart reader reading from r using the +// given MIME boundary. +// +// The boundary is usually obtained from the "boundary" parameter of +// the message's "Content-Type" header. Use mime.ParseMediaType to +// parse such headers. +func NewMultipartReader(r io.Reader, boundary string) *MultipartReader { + b := []byte("\r\n--" + boundary + "--") + return &MultipartReader{ + bufReader: bufio.NewReaderSize(&stickyErrorReader{r: r}, peekBufferSize), + nl: b[:2], + nlDashBoundary: b[:len(b)-2], + dashBoundaryDash: b[2:], + dashBoundary: b[2 : len(b)-2], + } +} + +// stickyErrorReader is an io.Reader which never calls Read on its +// underlying Reader once an error has been seen. (the io.Reader +// interface's contract promises nothing about the return values of +// Read calls after an error, yet this package does do multiple Reads +// after error) +type stickyErrorReader struct { + r io.Reader + err error +} + +func (r *stickyErrorReader) Read(p []byte) (n int, _ error) { + if r.err != nil { + return 0, r.err + } + n, r.err = r.r.Read(p) + return n, r.err +} + +func newPart(mr *MultipartReader) (*Part, error) { + bp := &Part{mr: mr} + if err := bp.populateHeaders(); err != nil { + return nil, err + } + bp.r = partReader{bp} + return bp, nil +} + +func (bp *Part) populateHeaders() error { + header, err := ReadHeader(bp.mr.bufReader) + if err == nil { + bp.Header = header + } + return err +} + +// Read reads the body of a part, after its headers and before the +// next part (if any) begins. +func (p *Part) Read(d []byte) (n int, err error) { + return p.r.Read(d) +} + +// partReader implements io.Reader by reading raw bytes directly from the +// wrapped *Part, without doing any Transfer-Encoding decoding. +type partReader struct { + p *Part +} + +func (pr partReader) Read(d []byte) (int, error) { + p := pr.p + br := p.mr.bufReader + + // Read into buffer until we identify some data to return, + // or we find a reason to stop (boundary or read error). + for p.n == 0 && p.err == nil { + peek, _ := br.Peek(br.Buffered()) + p.n, p.err = scanUntilBoundary(peek, p.mr.dashBoundary, p.mr.nlDashBoundary, p.total, p.readErr) + if p.n == 0 && p.err == nil { + // Force buffered I/O to read more into buffer. + _, p.readErr = br.Peek(len(peek) + 1) + if p.readErr == io.EOF { + p.readErr = io.ErrUnexpectedEOF + } + } + } + + // Read out from "data to return" part of buffer. + if p.n == 0 { + return 0, p.err + } + n := len(d) + if n > p.n { + n = p.n + } + n, _ = br.Read(d[:n]) + p.total += int64(n) + p.n -= n + if p.n == 0 { + return n, p.err + } + return n, nil +} + +// scanUntilBoundary scans buf to identify how much of it can be safely +// returned as part of the Part body. +// dashBoundary is "--boundary". +// nlDashBoundary is "\r\n--boundary" or "\n--boundary", depending on what mode we are in. +// The comments below (and the name) assume "\n--boundary", but either is accepted. +// total is the number of bytes read out so far. If total == 0, then a leading "--boundary" is recognized. +// readErr is the read error, if any, that followed reading the bytes in buf. +// scanUntilBoundary returns the number of data bytes from buf that can be +// returned as part of the Part body and also the error to return (if any) +// once those data bytes are done. +func scanUntilBoundary(buf, dashBoundary, nlDashBoundary []byte, total int64, readErr error) (int, error) { + if total == 0 { + // At beginning of body, allow dashBoundary. + if bytes.HasPrefix(buf, dashBoundary) { + switch matchAfterPrefix(buf, dashBoundary, readErr) { + case -1: + return len(dashBoundary), nil + case 0: + return 0, nil + case +1: + return 0, io.EOF + } + } + if bytes.HasPrefix(dashBoundary, buf) { + return 0, readErr + } + } + + // Search for "\n--boundary". + if i := bytes.Index(buf, nlDashBoundary); i >= 0 { + switch matchAfterPrefix(buf[i:], nlDashBoundary, readErr) { + case -1: + return i + len(nlDashBoundary), nil + case 0: + return i, nil + case +1: + return i, io.EOF + } + } + if bytes.HasPrefix(nlDashBoundary, buf) { + return 0, readErr + } + + // Otherwise, anything up to the final \n is not part of the boundary + // and so must be part of the body. + // Also if the section from the final \n onward is not a prefix of the boundary, + // it too must be part of the body. + i := bytes.LastIndexByte(buf, nlDashBoundary[0]) + if i >= 0 && bytes.HasPrefix(nlDashBoundary, buf[i:]) { + return i, nil + } + return len(buf), readErr +} + +// matchAfterPrefix checks whether buf should be considered to match the boundary. +// The prefix is "--boundary" or "\r\n--boundary" or "\n--boundary", +// and the caller has verified already that bytes.HasPrefix(buf, prefix) is true. +// +// matchAfterPrefix returns +1 if the buffer does match the boundary, +// meaning the prefix is followed by a dash, space, tab, cr, nl, or end of input. +// It returns -1 if the buffer definitely does NOT match the boundary, +// meaning the prefix is followed by some other character. +// For example, "--foobar" does not match "--foo". +// It returns 0 more input needs to be read to make the decision, +// meaning that len(buf) == len(prefix) and readErr == nil. +func matchAfterPrefix(buf, prefix []byte, readErr error) int { + if len(buf) == len(prefix) { + if readErr != nil { + return +1 + } + return 0 + } + c := buf[len(prefix)] + if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '-' { + return +1 + } + return -1 +} + +func (p *Part) Close() error { + io.Copy(ioutil.Discard, p) + return nil +} + +// MultipartReader is an iterator over parts in a MIME multipart body. +// MultipartReader's underlying parser consumes its input as needed. Seeking +// isn't supported. +type MultipartReader struct { + bufReader *bufio.Reader + + currentPart *Part + partsRead int + + nl []byte // "\r\n" or "\n" (set after seeing first boundary line) + nlDashBoundary []byte // nl + "--boundary" + dashBoundaryDash []byte // "--boundary--" + dashBoundary []byte // "--boundary" +} + +// NextPart returns the next part in the multipart or an error. +// When there are no more parts, the error io.EOF is returned. +func (r *MultipartReader) NextPart() (*Part, error) { + if r.currentPart != nil { + r.currentPart.Close() + } + if string(r.dashBoundary) == "--" { + return nil, fmt.Errorf("multipart: boundary is empty") + } + expectNewPart := false + for { + line, err := r.bufReader.ReadSlice('\n') + + if err == io.EOF && r.isFinalBoundary(line) { + // If the buffer ends in "--boundary--" without the + // trailing "\r\n", ReadSlice will return an error + // (since it's missing the '\n'), but this is a valid + // multipart EOF so we need to return io.EOF instead of + // a fmt-wrapped one. + return nil, io.EOF + } + if err != nil { + return nil, fmt.Errorf("multipart: NextPart: %v", err) + } + + if r.isBoundaryDelimiterLine(line) { + r.partsRead++ + bp, err := newPart(r) + if err != nil { + return nil, err + } + r.currentPart = bp + return bp, nil + } + + if r.isFinalBoundary(line) { + // Expected EOF + return nil, io.EOF + } + + if expectNewPart { + return nil, fmt.Errorf("multipart: expecting a new Part; got line %q", string(line)) + } + + if r.partsRead == 0 { + // skip line + continue + } + + // Consume the "\n" or "\r\n" separator between the + // body of the previous part and the boundary line we + // now expect will follow. (either a new part or the + // end boundary) + if bytes.Equal(line, r.nl) { + expectNewPart = true + continue + } + + return nil, fmt.Errorf("multipart: unexpected line in Next(): %q", line) + } +} + +// isFinalBoundary reports whether line is the final boundary line +// indicating that all parts are over. +// It matches `^--boundary--[ \t]*(\r\n)?$` +func (mr *MultipartReader) isFinalBoundary(line []byte) bool { + if !bytes.HasPrefix(line, mr.dashBoundaryDash) { + return false + } + rest := line[len(mr.dashBoundaryDash):] + rest = skipLWSPChar(rest) + return len(rest) == 0 || bytes.Equal(rest, mr.nl) +} + +func (mr *MultipartReader) isBoundaryDelimiterLine(line []byte) (ret bool) { + // https://tools.ietf.org/html/rfc2046#section-5.1 + // The boundary delimiter line is then defined as a line + // consisting entirely of two hyphen characters ("-", + // decimal value 45) followed by the boundary parameter + // value from the Content-Type header field, optional linear + // whitespace, and a terminating CRLF. + if !bytes.HasPrefix(line, mr.dashBoundary) { + return false + } + rest := line[len(mr.dashBoundary):] + rest = skipLWSPChar(rest) + + // On the first part, see our lines are ending in \n instead of \r\n + // and switch into that mode if so. This is a violation of the spec, + // but occurs in practice. + if mr.partsRead == 0 && len(rest) == 1 && rest[0] == '\n' { + mr.nl = mr.nl[1:] + mr.nlDashBoundary = mr.nlDashBoundary[1:] + } + return bytes.Equal(rest, mr.nl) +} + +// skipLWSPChar returns b with leading spaces and tabs removed. +// RFC 822 defines: +// +// LWSP-char = SPACE / HTAB +func skipLWSPChar(b []byte) []byte { + for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') { + b = b[1:] + } + return b +} + +// A MultipartWriter generates multipart messages. +type MultipartWriter struct { + w io.Writer + boundary string + lastpart *part +} + +// NewMultipartWriter returns a new multipart Writer with a random boundary, +// writing to w. +func NewMultipartWriter(w io.Writer) *MultipartWriter { + return &MultipartWriter{ + w: w, + boundary: randomBoundary(), + } +} + +// Boundary returns the Writer's boundary. +func (w *MultipartWriter) Boundary() string { + return w.boundary +} + +// SetBoundary overrides the Writer's default randomly-generated +// boundary separator with an explicit value. +// +// SetBoundary must be called before any parts are created, may only +// contain certain ASCII characters, and must be non-empty and +// at most 70 bytes long. +func (w *MultipartWriter) SetBoundary(boundary string) error { + if w.lastpart != nil { + return errors.New("mime: SetBoundary called after write") + } + // rfc2046#section-5.1.1 + if len(boundary) < 1 || len(boundary) > 70 { + return errors.New("mime: invalid boundary length") + } + end := len(boundary) - 1 + for i, b := range boundary { + if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' { + continue + } + switch b { + case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?': + continue + case ' ': + if i != end { + continue + } + } + return errors.New("mime: invalid boundary character") + } + w.boundary = boundary + return nil +} + +func randomBoundary() string { + var buf [30]byte + _, err := io.ReadFull(rand.Reader, buf[:]) + if err != nil { + panic(err) + } + return fmt.Sprintf("%x", buf[:]) +} + +// CreatePart creates a new multipart section with the provided +// header. The body of the part should be written to the returned +// Writer. After calling CreatePart, any previous part may no longer +// be written to. +func (w *MultipartWriter) CreatePart(header Header) (io.Writer, error) { + if w.lastpart != nil { + if err := w.lastpart.close(); err != nil { + return nil, err + } + } + var b bytes.Buffer + if w.lastpart != nil { + fmt.Fprintf(&b, "\r\n--%s\r\n", w.boundary) + } else { + fmt.Fprintf(&b, "--%s\r\n", w.boundary) + } + + WriteHeader(&b, header) + + _, err := io.Copy(w.w, &b) + if err != nil { + return nil, err + } + p := &part{ + mw: w, + } + w.lastpart = p + return p, nil +} + +// Close finishes the multipart message and writes the trailing +// boundary end line to the output. +func (w *MultipartWriter) Close() error { + if w.lastpart != nil { + if err := w.lastpart.close(); err != nil { + return err + } + w.lastpart = nil + } + _, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary) + return err +} + +type part struct { + mw *MultipartWriter + closed bool + we error // last error that occurred writing +} + +func (p *part) close() error { + p.closed = true + return p.we +} + +func (p *part) Write(d []byte) (n int, err error) { + if p.closed { + return 0, errors.New("multipart: can't write to finished part") + } + n, err = p.mw.w.Write(d) + if err != nil { + p.we = err + } + return +} diff --git a/vendor/github.com/emersion/go-message/textproto/textproto.go b/vendor/github.com/emersion/go-message/textproto/textproto.go new file mode 100644 index 000000000..2fa994bd7 --- /dev/null +++ b/vendor/github.com/emersion/go-message/textproto/textproto.go @@ -0,0 +1,2 @@ +// Package textproto implements low-level manipulation of MIME messages. +package textproto diff --git a/vendor/github.com/emersion/go-message/writer.go b/vendor/github.com/emersion/go-message/writer.go new file mode 100644 index 000000000..6a80da2b3 --- /dev/null +++ b/vendor/github.com/emersion/go-message/writer.go @@ -0,0 +1,134 @@ +package message + +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/emersion/go-message/textproto" +) + +// Writer writes message entities. +// +// If the message is not multipart, it should be used as a WriteCloser. Don't +// forget to call Close. +// +// If the message is multipart, users can either use CreatePart to write child +// parts or Write to directly pipe a multipart message. In any case, Close must +// be called at the end. +type Writer struct { + w io.Writer + c io.Closer + mw *textproto.MultipartWriter +} + +// createWriter creates a new Writer writing to w with the provided header. +// Nothing is written to w when it is called. header is modified in-place. +func createWriter(w io.Writer, header *Header) (*Writer, error) { + ww := &Writer{w: w} + + mediaType, mediaParams, _ := header.ContentType() + if strings.HasPrefix(mediaType, "multipart/") { + ww.mw = textproto.NewMultipartWriter(ww.w) + + // Do not set ww's io.Closer for now: if this is a multipart entity but + // CreatePart is not used (only Write is used), then the final boundary + // is expected to be written by the user too. In this case, ww.Close + // shouldn't write the final boundary. + + if mediaParams["boundary"] != "" { + ww.mw.SetBoundary(mediaParams["boundary"]) + } else { + mediaParams["boundary"] = ww.mw.Boundary() + header.SetContentType(mediaType, mediaParams) + } + + header.Del("Content-Transfer-Encoding") + } else { + wc, err := encodingWriter(header.Get("Content-Transfer-Encoding"), ww.w) + if err != nil { + return nil, err + } + ww.w = wc + ww.c = wc + } + + switch strings.ToLower(mediaParams["charset"]) { + case "", "us-ascii", "utf-8": + // This is OK + default: + // Anything else is invalid + return nil, fmt.Errorf("unhandled charset %q", mediaParams["charset"]) + } + + return ww, nil +} + +// CreateWriter creates a new message writer to w. If header contains an +// encoding, data written to the Writer will automatically be encoded with it. +// The charset needs to be utf-8 or us-ascii. +func CreateWriter(w io.Writer, header Header) (*Writer, error) { + // Ensure that modifications are invisible to the caller + header = header.Copy() + + // If the message uses MIME, it has to include MIME-Version + if !header.Has("Mime-Version") { + header.Set("MIME-Version", "1.0") + } + + ww, err := createWriter(w, &header) + if err != nil { + return nil, err + } + if err := textproto.WriteHeader(w, header.Header); err != nil { + return nil, err + } + return ww, nil +} + +// Write implements io.Writer. +func (w *Writer) Write(b []byte) (int, error) { + return w.w.Write(b) +} + +// Close implements io.Closer. +func (w *Writer) Close() error { + if w.c != nil { + return w.c.Close() + } + return nil +} + +// CreatePart returns a Writer to a new part in this multipart entity. If this +// entity is not multipart, it fails. The body of the part should be written to +// the returned io.WriteCloser. +func (w *Writer) CreatePart(header Header) (*Writer, error) { + if w.mw == nil { + return nil, errors.New("cannot create a part in a non-multipart message") + } + + if w.c == nil { + // We know that the user calls CreatePart so Close should write the final + // boundary + w.c = w.mw + } + + // cw -> ww -> pw -> w.mw -> w.w + + ww := &struct{ io.Writer }{nil} + + // ensure that modifications are invisible to the caller + header = header.Copy() + cw, err := createWriter(ww, &header) + if err != nil { + return nil, err + } + pw, err := w.mw.CreatePart(header.Header) + if err != nil { + return nil, err + } + + ww.Writer = pw + return cw, nil +} diff --git a/vendor/github.com/emersion/go-sasl/.build.yml b/vendor/github.com/emersion/go-sasl/.build.yml new file mode 100644 index 000000000..daa6006df --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/.build.yml @@ -0,0 +1,19 @@ +image: alpine/latest +packages: + - go + # Required by codecov + - bash + - findutils +sources: + - https://github.com/emersion/go-sasl +tasks: + - build: | + cd go-sasl + go build -v ./... + - test: | + cd go-sasl + go test -coverprofile=coverage.txt -covermode=atomic ./... + - upload-coverage: | + cd go-sasl + export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1 + curl -s https://codecov.io/bash | bash diff --git a/vendor/github.com/emersion/go-sasl/.gitignore b/vendor/github.com/emersion/go-sasl/.gitignore new file mode 100644 index 000000000..daf913b1b --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/emersion/go-sasl/LICENSE b/vendor/github.com/emersion/go-sasl/LICENSE new file mode 100644 index 000000000..dc1922e47 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 emersion + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/emersion/go-sasl/README.md b/vendor/github.com/emersion/go-sasl/README.md new file mode 100644 index 000000000..6bd47ba81 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/README.md @@ -0,0 +1,18 @@ +# go-sasl + +[![godocs.io](https://godocs.io/github.com/emersion/go-sasl?status.svg)](https://godocs.io/github.com/emersion/go-sasl) +[![Build Status](https://travis-ci.org/emersion/go-sasl.svg?branch=master)](https://travis-ci.org/emersion/go-sasl) + +A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go. + +Implemented mechanisms: + +* [ANONYMOUS](https://tools.ietf.org/html/rfc4505) +* [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A) +* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead) +* [PLAIN](https://tools.ietf.org/html/rfc4616) +* [OAUTHBEARER](https://tools.ietf.org/html/rfc7628) + +## License + +MIT diff --git a/vendor/github.com/emersion/go-sasl/anonymous.go b/vendor/github.com/emersion/go-sasl/anonymous.go new file mode 100644 index 000000000..abcb753e6 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/anonymous.go @@ -0,0 +1,56 @@ +package sasl + +// The ANONYMOUS mechanism name. +const Anonymous = "ANONYMOUS" + +type anonymousClient struct { + Trace string +} + +func (c *anonymousClient) Start() (mech string, ir []byte, err error) { + mech = Anonymous + ir = []byte(c.Trace) + return +} + +func (c *anonymousClient) Next(challenge []byte) (response []byte, err error) { + return nil, ErrUnexpectedServerChallenge +} + +// A client implementation of the ANONYMOUS authentication mechanism, as +// described in RFC 4505. +func NewAnonymousClient(trace string) Client { + return &anonymousClient{trace} +} + +// Get trace information from clients logging in anonymously. +type AnonymousAuthenticator func(trace string) error + +type anonymousServer struct { + done bool + authenticate AnonymousAuthenticator +} + +func (s *anonymousServer) Next(response []byte) (challenge []byte, done bool, err error) { + if s.done { + err = ErrUnexpectedClientResponse + return + } + + // No initial response, send an empty challenge + if response == nil { + return []byte{}, false, nil + } + + s.done = true + + err = s.authenticate(string(response)) + done = true + return +} + +// A server implementation of the ANONYMOUS authentication mechanism, as +// described in RFC 4505. +func NewAnonymousServer(authenticator AnonymousAuthenticator) Server { + return &anonymousServer{authenticate: authenticator} +} diff --git a/vendor/github.com/emersion/go-sasl/external.go b/vendor/github.com/emersion/go-sasl/external.go new file mode 100644 index 000000000..ba24ccc0f --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/external.go @@ -0,0 +1,67 @@ +package sasl + +import ( + "bytes" + "errors" +) + +// The EXTERNAL mechanism name. +const External = "EXTERNAL" + +type externalClient struct { + Identity string +} + +func (a *externalClient) Start() (mech string, ir []byte, err error) { + mech = External + ir = []byte(a.Identity) + return +} + +func (a *externalClient) Next(challenge []byte) (response []byte, err error) { + return nil, ErrUnexpectedServerChallenge +} + +// An implementation of the EXTERNAL authentication mechanism, as described in +// RFC 4422. Authorization identity may be left blank to indicate that the +// client is requesting to act as the identity associated with the +// authentication credentials. +func NewExternalClient(identity string) Client { + return &externalClient{identity} +} + +// ExternalAuthenticator authenticates users with the EXTERNAL mechanism. If +// the identity is left blank, it indicates that it is the same as the one used +// in the external credentials. If identity is not empty and the server doesn't +// support it, an error must be returned. +type ExternalAuthenticator func(identity string) error + +type externalServer struct { + done bool + authenticate ExternalAuthenticator +} + +func (a *externalServer) Next(response []byte) (challenge []byte, done bool, err error) { + if a.done { + return nil, false, ErrUnexpectedClientResponse + } + + // No initial response, send an empty challenge + if response == nil { + return []byte{}, false, nil + } + + a.done = true + + if bytes.Contains(response, []byte("\x00")) { + return nil, false, errors.New("sasl: identity contains a NUL character") + } + + return nil, true, a.authenticate(string(response)) +} + +// NewExternalServer creates a server implementation of the EXTERNAL +// authentication mechanism, as described in RFC 4422. +func NewExternalServer(authenticator ExternalAuthenticator) Server { + return &externalServer{authenticate: authenticator} +} diff --git a/vendor/github.com/emersion/go-sasl/login.go b/vendor/github.com/emersion/go-sasl/login.go new file mode 100644 index 000000000..3847ee146 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/login.go @@ -0,0 +1,89 @@ +package sasl + +import ( + "bytes" +) + +// The LOGIN mechanism name. +const Login = "LOGIN" + +var expectedChallenge = []byte("Password:") + +type loginClient struct { + Username string + Password string +} + +func (a *loginClient) Start() (mech string, ir []byte, err error) { + mech = "LOGIN" + ir = []byte(a.Username) + return +} + +func (a *loginClient) Next(challenge []byte) (response []byte, err error) { + if bytes.Compare(challenge, expectedChallenge) != 0 { + return nil, ErrUnexpectedServerChallenge + } else { + return []byte(a.Password), nil + } +} + +// A client implementation of the LOGIN authentication mechanism for SMTP, +// as described in http://www.iana.org/go/draft-murchison-sasl-login +// +// It is considered obsolete, and should not be used when other mechanisms are +// available. For plaintext password authentication use PLAIN mechanism. +func NewLoginClient(username, password string) Client { + return &loginClient{username, password} +} + +// Authenticates users with an username and a password. +type LoginAuthenticator func(username, password string) error + +type loginState int + +const ( + loginNotStarted loginState = iota + loginWaitingUsername + loginWaitingPassword +) + +type loginServer struct { + state loginState + username, password string + authenticate LoginAuthenticator +} + +// A server implementation of the LOGIN authentication mechanism, as described +// in https://tools.ietf.org/html/draft-murchison-sasl-login-00. +// +// LOGIN is obsolete and should only be enabled for legacy clients that cannot +// be updated to use PLAIN. +func NewLoginServer(authenticator LoginAuthenticator) Server { + return &loginServer{authenticate: authenticator} +} + +func (a *loginServer) Next(response []byte) (challenge []byte, done bool, err error) { + switch a.state { + case loginNotStarted: + // Check for initial response field, as per RFC4422 section 3 + if response == nil { + challenge = []byte("Username:") + break + } + a.state++ + fallthrough + case loginWaitingUsername: + a.username = string(response) + challenge = []byte("Password:") + case loginWaitingPassword: + a.password = string(response) + err = a.authenticate(a.username, a.password) + done = true + default: + err = ErrUnexpectedClientResponse + } + + a.state++ + return +} diff --git a/vendor/github.com/emersion/go-sasl/oauthbearer.go b/vendor/github.com/emersion/go-sasl/oauthbearer.go new file mode 100644 index 000000000..7b2c503c6 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/oauthbearer.go @@ -0,0 +1,198 @@ +package sasl + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" +) + +// The OAUTHBEARER mechanism name. +const OAuthBearer = "OAUTHBEARER" + +type OAuthBearerError struct { + Status string `json:"status"` + Schemes string `json:"schemes"` + Scope string `json:"scope"` +} + +type OAuthBearerOptions struct { + Username string + Token string + Host string + Port int +} + +// Implements error +func (err *OAuthBearerError) Error() string { + return fmt.Sprintf("OAUTHBEARER authentication error (%v)", err.Status) +} + +type oauthBearerClient struct { + OAuthBearerOptions +} + +func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) { + var authzid string + if a.Username != "" { + authzid = "a=" + a.Username + } + str := "n," + authzid + "," + + if a.Host != "" { + str += "\x01host=" + a.Host + } + + if a.Port != 0 { + str += "\x01port=" + strconv.Itoa(a.Port) + } + str += "\x01auth=Bearer " + a.Token + "\x01\x01" + ir = []byte(str) + return OAuthBearer, ir, nil +} + +func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) { + authBearerErr := &OAuthBearerError{} + if err := json.Unmarshal(challenge, authBearerErr); err != nil { + return nil, err + } else { + return nil, authBearerErr + } +} + +// An implementation of the OAUTHBEARER authentication mechanism, as +// described in RFC 7628. +func NewOAuthBearerClient(opt *OAuthBearerOptions) Client { + return &oauthBearerClient{*opt} +} + +type OAuthBearerAuthenticator func(opts OAuthBearerOptions) *OAuthBearerError + +type oauthBearerServer struct { + done bool + failErr error + authenticate OAuthBearerAuthenticator +} + +func (a *oauthBearerServer) fail(descr string) ([]byte, bool, error) { + blob, err := json.Marshal(OAuthBearerError{ + Status: "invalid_request", + Schemes: "bearer", + }) + if err != nil { + panic(err) // wtf + } + a.failErr = errors.New("sasl: client error: " + descr) + return blob, false, nil +} + +func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool, err error) { + // Per RFC, we cannot just send an error, we need to return JSON-structured + // value as a challenge and then after getting dummy response from the + // client stop the exchange. + if a.failErr != nil { + // Server libraries (go-smtp, go-imap) will not call Next on + // protocol-specific SASL cancel response ('*'). However, GS2 (and + // indirectly OAUTHBEARER) defines a protocol-independent way to do so + // using 0x01. + if len(response) != 1 && response[0] != 0x01 { + return nil, true, errors.New("sasl: invalid response") + } + return nil, true, a.failErr + } + + if a.done { + err = ErrUnexpectedClientResponse + return + } + + // Generate empty challenge. + if response == nil { + return []byte{}, false, nil + } + + a.done = true + + // Cut n,a=username,\x01host=...\x01auth=... + // into + // n + // a=username + // \x01host=...\x01auth=...\x01\x01 + parts := bytes.SplitN(response, []byte{','}, 3) + if len(parts) != 3 { + return a.fail("Invalid response") + } + flag := parts[0] + authzid := parts[1] + if !bytes.Equal(flag, []byte{'n'}) { + return a.fail("Invalid response, missing 'n' in gs2-cb-flag") + } + opts := OAuthBearerOptions{} + if len(authzid) > 0 { + if !bytes.HasPrefix(authzid, []byte("a=")) { + return a.fail("Invalid response, missing 'a=' in gs2-authzid") + } + opts.Username = string(bytes.TrimPrefix(authzid, []byte("a="))) + } + + // Cut \x01host=...\x01auth=...\x01\x01 + // into + // *empty* + // host=... + // auth=... + // *empty* + // + // Note that this code does not do a lot of checks to make sure the input + // follows the exact format specified by RFC. + params := bytes.Split(parts[2], []byte{0x01}) + for _, p := range params { + // Skip empty fields (one at start and end). + if len(p) == 0 { + continue + } + + pParts := bytes.SplitN(p, []byte{'='}, 2) + if len(pParts) != 2 { + return a.fail("Invalid response, missing '='") + } + + switch string(pParts[0]) { + case "host": + opts.Host = string(pParts[1]) + case "port": + port, err := strconv.ParseUint(string(pParts[1]), 10, 16) + if err != nil { + return a.fail("Invalid response, malformed 'port' value") + } + opts.Port = int(port) + case "auth": + const prefix = "bearer " + strValue := string(pParts[1]) + // Token type is case-insensitive. + if !strings.HasPrefix(strings.ToLower(strValue), prefix) { + return a.fail("Unsupported token type") + } + opts.Token = strValue[len(prefix):] + default: + return a.fail("Invalid response, unknown parameter: " + string(pParts[0])) + } + } + + authzErr := a.authenticate(opts) + if authzErr != nil { + blob, err := json.Marshal(authzErr) + if err != nil { + panic(err) // wtf + } + a.failErr = authzErr + return blob, false, nil + } + + return nil, true, nil +} + +func NewOAuthBearerServer(auth OAuthBearerAuthenticator) Server { + return &oauthBearerServer{authenticate: auth} +} diff --git a/vendor/github.com/emersion/go-sasl/plain.go b/vendor/github.com/emersion/go-sasl/plain.go new file mode 100644 index 000000000..5017bdc49 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/plain.go @@ -0,0 +1,77 @@ +package sasl + +import ( + "bytes" + "errors" +) + +// The PLAIN mechanism name. +const Plain = "PLAIN" + +type plainClient struct { + Identity string + Username string + Password string +} + +func (a *plainClient) Start() (mech string, ir []byte, err error) { + mech = "PLAIN" + ir = []byte(a.Identity + "\x00" + a.Username + "\x00" + a.Password) + return +} + +func (a *plainClient) Next(challenge []byte) (response []byte, err error) { + return nil, ErrUnexpectedServerChallenge +} + +// A client implementation of the PLAIN authentication mechanism, as described +// in RFC 4616. Authorization identity may be left blank to indicate that it is +// the same as the username. +func NewPlainClient(identity, username, password string) Client { + return &plainClient{identity, username, password} +} + +// Authenticates users with an identity, a username and a password. If the +// identity is left blank, it indicates that it is the same as the username. +// If identity is not empty and the server doesn't support it, an error must be +// returned. +type PlainAuthenticator func(identity, username, password string) error + +type plainServer struct { + done bool + authenticate PlainAuthenticator +} + +func (a *plainServer) Next(response []byte) (challenge []byte, done bool, err error) { + if a.done { + err = ErrUnexpectedClientResponse + return + } + + // No initial response, send an empty challenge + if response == nil { + return []byte{}, false, nil + } + + a.done = true + + parts := bytes.Split(response, []byte("\x00")) + if len(parts) != 3 { + err = errors.New("sasl: invalid response") + return + } + + identity := string(parts[0]) + username := string(parts[1]) + password := string(parts[2]) + + err = a.authenticate(identity, username, password) + done = true + return +} + +// A server implementation of the PLAIN authentication mechanism, as described +// in RFC 4616. +func NewPlainServer(authenticator PlainAuthenticator) Server { + return &plainServer{authenticate: authenticator} +} diff --git a/vendor/github.com/emersion/go-sasl/sasl.go b/vendor/github.com/emersion/go-sasl/sasl.go new file mode 100644 index 000000000..525da8846 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/sasl.go @@ -0,0 +1,45 @@ +// Library for Simple Authentication and Security Layer (SASL) defined in RFC 4422. +package sasl + +// Note: +// Most of this code was copied, with some modifications, from net/smtp. It +// would be better if Go provided a standard package (e.g. crypto/sasl) that +// could be shared by SMTP, IMAP, and other packages. + +import ( + "errors" +) + +// Common SASL errors. +var ( + ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response") + ErrUnexpectedServerChallenge = errors.New("sasl: unexpected server challenge") +) + +// Client interface to perform challenge-response authentication. +type Client interface { + // Begins SASL authentication with the server. It returns the + // authentication mechanism name and "initial response" data (if required by + // the selected mechanism). A non-nil error causes the client to abort the + // authentication attempt. + // + // A nil ir value is different from a zero-length value. The nil value + // indicates that the selected mechanism does not use an initial response, + // while a zero-length value indicates an empty initial response, which must + // be sent to the server. + Start() (mech string, ir []byte, err error) + + // Continues challenge-response authentication. A non-nil error causes + // the client to abort the authentication attempt. + Next(challenge []byte) (response []byte, err error) +} + +// Server interface to perform challenge-response authentication. +type Server interface { + // Begins or continues challenge-response authentication. If the client + // supplies an initial response, response is non-nil. + // + // If the authentication is finished, done is set to true. If the + // authentication has failed, an error is returned. + Next(response []byte) (challenge []byte, done bool, err error) +} diff --git a/vendor/github.com/go-crypt/crypt/LICENSE b/vendor/github.com/go-crypt/crypt/LICENSE new file mode 100644 index 000000000..a149832c0 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 github.com/go-crypt/crypt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/go-crypt/crypt/algorithm/const.go b/vendor/github.com/go-crypt/crypt/algorithm/const.go new file mode 100644 index 000000000..08c585b4d --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/const.go @@ -0,0 +1,26 @@ +package algorithm + +const ( + // DigestSHA1 is te name for SHA1 digests. + DigestSHA1 = "sha1" + + // DigestSHA224 is te name for SHA224 digests. + DigestSHA224 = "sha224" + + // DigestSHA256 is te name for SHA256 digests. + DigestSHA256 = "sha256" + + // DigestSHA384 is te name for SHA384 digests. + DigestSHA384 = "sha384" + + // DigestSHA512 is te name for SHA512 digests. + DigestSHA512 = "sha512" +) + +const ( + // SaltLengthDefault is the default salt size for most implementations. + SaltLengthDefault = 16 + + // KeyLengthDefault is the default key size for most implementations. + KeyLengthDefault = 32 +) diff --git a/vendor/github.com/go-crypt/crypt/algorithm/doc.go b/vendor/github.com/go-crypt/crypt/algorithm/doc.go new file mode 100644 index 000000000..35a89bc55 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/doc.go @@ -0,0 +1,3 @@ +// Package algorithm is a package which contains the individual algorithms and interfaces related to their +// implementation. +package algorithm diff --git a/vendor/github.com/go-crypt/crypt/algorithm/errors.go b/vendor/github.com/go-crypt/crypt/algorithm/errors.go new file mode 100644 index 000000000..2cec6fc8a --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/errors.go @@ -0,0 +1,66 @@ +package algorithm + +import ( + "errors" +) + +var ( + // ErrEncodedHashInvalidFormat is an error returned when an encoded hash has an invalid format. + ErrEncodedHashInvalidFormat = errors.New("provided encoded hash has an invalid format") + + // ErrEncodedHashInvalidIdentifier is an error returned when an encoded hash has an invalid identifier for the + // given digest. + ErrEncodedHashInvalidIdentifier = errors.New("provided encoded hash has an invalid identifier") + + // ErrEncodedHashInvalidVersion is an error returned when an encoded hash has an unsupported or otherwise invalid + // version. + ErrEncodedHashInvalidVersion = errors.New("provided encoded hash has an invalid version") + + // ErrEncodedHashInvalidOption is an error returned when an encoded hash has an unsupported or otherwise invalid + // option in the option field. + ErrEncodedHashInvalidOption = errors.New("provided encoded hash has an invalid option") + + // ErrEncodedHashInvalidOptionKey is an error returned when an encoded hash has an unknown or otherwise invalid + // option key in the option field. + ErrEncodedHashInvalidOptionKey = errors.New("provided encoded hash has an invalid option key") + + // ErrEncodedHashInvalidOptionValue is an error returned when an encoded hash has an unknown or otherwise invalid + // option value in the option field. + ErrEncodedHashInvalidOptionValue = errors.New("provided encoded hash has an invalid option value") + + // ErrEncodedHashKeyEncoding is an error returned when an encoded hash has a salt with an invalid or unsupported + // encoding. + ErrEncodedHashKeyEncoding = errors.New("provided encoded hash has a key value that can't be decoded") + + // ErrEncodedHashSaltEncoding is an error returned when an encoded hash has a salt with an invalid or unsupported + // encoding. + ErrEncodedHashSaltEncoding = errors.New("provided encoded hash has a salt value that can't be decoded") + + // ErrKeyDerivation is returned when a Key function returns an error. + ErrKeyDerivation = errors.New("failed to derive the key with the provided parameters") + + // ErrSaltEncoding is an error returned when a salt has an invalid or unsupported encoding. + ErrSaltEncoding = errors.New("provided salt has a value that can't be decoded") + + // ErrPasswordInvalid is an error returned when a password has an invalid or unsupported properties. It is NOT + // returned on password mismatches. + ErrPasswordInvalid = errors.New("password is invalid") + + // ErrSaltInvalid is an error returned when a salt has an invalid or unsupported properties. + ErrSaltInvalid = errors.New("salt is invalid") + + // ErrSaltReadRandomBytes is an error returned when generating the random bytes for salt resulted in an error. + ErrSaltReadRandomBytes = errors.New("could not read random bytes for salt") + + // ErrParameterInvalid is an error returned when a parameter has an invalid value. + ErrParameterInvalid = errors.New("parameter is invalid") +) + +// Error format strings. +const ( + ErrFmtInvalidIntParameter = "%w: parameter '%s' must be between %d%s and %d but is set to '%d'" + ErrFmtDigestDecode = "%s decode error: %w" + ErrFmtDigestMatch = "%s match error: %w" + ErrFmtHasherHash = "%s hashing error: %w" + ErrFmtHasherValidation = "%s validation error: %w" +) diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/const.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/const.go new file mode 100644 index 000000000..b7906462b --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/const.go @@ -0,0 +1,46 @@ +package shacrypt + +const ( + // EncodingFmt is the encoding format for this algorithm. + EncodingFmt = "$%s$rounds=%d$%s$%s" + + // EncodingFmtRoundsOmitted is the encoding format for this algorithm when the rounds can be omitted. + EncodingFmtRoundsOmitted = "$%s$%s$%s" + + // AlgName is the name for this algorithm. + AlgName = "shacrypt" + + // AlgIdentifierSHA256 is the identifier used in encoded SHA256 variants of this algorithm. + AlgIdentifierSHA256 = "5" + + // AlgIdentifierSHA512 is the identifier used in encoded SHA512 variants of this algorithm. + AlgIdentifierSHA512 = "6" + + // IterationsMin is the minimum number of iterations accepted. + IterationsMin = 1000 + + // IterationsMax is the maximum number of iterations accepted. + IterationsMax = 999999999 + + // IterationsDefaultSHA256 is the default number of iterations for SHA256. + IterationsDefaultSHA256 = 1000000 + + // IterationsDefaultSHA512 is the default number of iterations for SHA512. + IterationsDefaultSHA512 = 500000 + + // IterationsDefaultOmitted is the default number of iterations when the rounds are omitted. + IterationsDefaultOmitted = 5000 + + // SaltLengthMin is the minimum salt length. + SaltLengthMin = 1 + + // SaltLengthMax is the maximum salt length. + SaltLengthMax = 16 + + // SaltCharSet are the valid characters for the salt. + SaltCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./" +) + +const ( + variantDefault = VariantSHA512 +) diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/decoder.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/decoder.go new file mode 100644 index 000000000..a0fb81a83 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/decoder.go @@ -0,0 +1,136 @@ +package shacrypt + +import ( + "fmt" + "strconv" + + "github.com/go-crypt/crypt/algorithm" + "github.com/go-crypt/crypt/internal/encoding" +) + +// RegisterDecoder the decoder with the algorithm.DecoderRegister. +func RegisterDecoder(r algorithm.DecoderRegister) (err error) { + if err = RegisterDecoderSHA256(r); err != nil { + return err + } + + if err = RegisterDecoderSHA512(r); err != nil { + return err + } + + return nil +} + +// RegisterDecoderSHA256 registers specifically the sha256 decoder variant with the algorithm.DecoderRegister. +func RegisterDecoderSHA256(r algorithm.DecoderRegister) (err error) { + if err = r.RegisterDecodeFunc(VariantSHA256.Prefix(), DecodeVariant(VariantSHA256)); err != nil { + return err + } + + return nil +} + +// RegisterDecoderSHA512 registers specifically the sha512 decoder variant with the algorithm.DecoderRegister. +func RegisterDecoderSHA512(r algorithm.DecoderRegister) (err error) { + if err = r.RegisterDecodeFunc(VariantSHA512.Prefix(), DecodeVariant(VariantSHA512)); err != nil { + return err + } + + return nil +} + +// Decode the encoded digest into a algorithm.Digest. +func Decode(encodedDigest string) (digest algorithm.Digest, err error) { + return DecodeVariant(VariantNone)(encodedDigest) +} + +// DecodeVariant the encoded digest into a algorithm.Digest provided it matches the provided Variant. If VariantNone is +// used all variants can be decoded. +func DecodeVariant(v Variant) func(encodedDigest string) (digest algorithm.Digest, err error) { + return func(encodedDigest string) (digest algorithm.Digest, err error) { + var ( + parts []string + variant Variant + ) + + if variant, parts, err = decoderParts(encodedDigest); err != nil { + return nil, fmt.Errorf(algorithm.ErrFmtDigestDecode, AlgName, err) + } + + if v != VariantNone && v != variant { + return nil, fmt.Errorf(algorithm.ErrFmtDigestDecode, AlgName, fmt.Errorf("the '%s' variant cannot be decoded only the '%s' variant can be", variant.String(), v.String())) + } + + if digest, err = decode(variant, parts); err != nil { + return nil, fmt.Errorf(algorithm.ErrFmtDigestDecode, AlgName, err) + } + + return digest, nil + } +} + +func decoderParts(encodedDigest string) (variant Variant, parts []string, err error) { + parts = encoding.Split(encodedDigest, -1) + + if n := len(parts); n != 4 && n != 5 { + return VariantNone, nil, algorithm.ErrEncodedHashInvalidFormat + } + + variant = NewVariant(parts[1]) + + if variant == VariantNone { + return variant, nil, fmt.Errorf("%w: identifier '%s' is not an encoded %s digest", algorithm.ErrEncodedHashInvalidIdentifier, parts[1], AlgName) + } + + return variant, parts[2:], nil +} + +func decode(variant Variant, parts []string) (digest algorithm.Digest, err error) { + decoded := &Digest{ + variant: variant, + } + + var ( + ip, is, ik int + ) + + switch len(parts) { + case 2: + ip, is, ik = -1, 0, 1 + case 3: + ip, is, ik = 0, 1, 2 + } + + if len(parts[ik]) == 0 { + return nil, fmt.Errorf("%w: key has 0 bytes", algorithm.ErrEncodedHashKeyEncoding) + } + + decoded.iterations = IterationsDefaultOmitted + + var params []encoding.Parameter + + if ip >= 0 { + if params, err = encoding.DecodeParameterStr(parts[ip]); err != nil { + return nil, err + } + } + + for _, param := range params { + switch param.Key { + case "rounds": + var rounds uint64 + + if rounds, err = strconv.ParseUint(param.Value, 10, 32); err != nil { + return nil, fmt.Errorf("%w: option '%s' has invalid value '%s': %v", algorithm.ErrEncodedHashInvalidOptionValue, param.Key, param.Value, err) + } + + decoded.iterations = int(rounds) + default: + return nil, fmt.Errorf("%w: option '%s' with value '%s' is unknown", algorithm.ErrEncodedHashInvalidOptionKey, param.Key, param.Value) + } + } + + decoded.salt, decoded.key = []byte(parts[is]), []byte(parts[ik]) + + return decoded, nil +} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/digest.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/digest.go new file mode 100644 index 000000000..46a760220 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/digest.go @@ -0,0 +1,83 @@ +package shacrypt + +import ( + "crypto/subtle" + "fmt" + "strings" + + xcrypt "github.com/go-crypt/x/crypt" + + "github.com/go-crypt/crypt/algorithm" +) + +// Digest is a digest which handles SHA-crypt hashes like SHA256 or SHA512. +type Digest struct { + variant Variant + + iterations int + salt, key []byte +} + +// Match returns true if the string password matches the current shacrypt.Digest. +func (d *Digest) Match(password string) (match bool) { + return d.MatchBytes([]byte(password)) +} + +// MatchBytes returns true if the []byte passwordBytes matches the current shacrypt.Digest. +func (d *Digest) MatchBytes(passwordBytes []byte) (match bool) { + match, _ = d.MatchBytesAdvanced(passwordBytes) + + return match +} + +// MatchAdvanced is the same as Match except if there is an error it returns that as well. +func (d *Digest) MatchAdvanced(password string) (match bool, err error) { + if match, err = d.MatchBytesAdvanced([]byte(password)); err != nil { + return match, fmt.Errorf(algorithm.ErrFmtDigestMatch, AlgName, err) + } + + return match, nil +} + +// MatchBytesAdvanced is the same as MatchBytes except if there is an error it returns that as well. +func (d *Digest) MatchBytesAdvanced(passwordBytes []byte) (match bool, err error) { + if len(d.key) == 0 { + return false, fmt.Errorf("%w: key has 0 bytes", algorithm.ErrPasswordInvalid) + } + + return subtle.ConstantTimeCompare(d.key, xcrypt.KeySHACrypt(d.variant.HashFunc(), passwordBytes, d.salt, d.iterations)) == 1, nil +} + +// Encode this Digest as a string for storage. +func (d *Digest) Encode() (hash string) { + switch d.iterations { + case IterationsDefaultOmitted: + return strings.ReplaceAll(fmt.Sprintf(EncodingFmtRoundsOmitted, + d.variant.Prefix(), + d.salt, d.key, + ), "\n", "") + default: + return strings.ReplaceAll(fmt.Sprintf(EncodingFmt, + d.variant.Prefix(), d.iterations, + d.salt, d.key, + ), "\n", "") + } +} + +// String returns the storable format of the shacrypt.Digest hash utilizing fmt.Sprintf and shacrypt.EncodingFmt. +func (d *Digest) String() string { + return d.Encode() +} + +func (d *Digest) defaults() { + switch d.variant { + case VariantSHA256, VariantSHA512: + break + default: + d.variant = variantDefault + } + + if d.iterations == 0 { + d.iterations = d.variant.DefaultIterations() + } +} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/doc.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/doc.go new file mode 100644 index 000000000..4ac7c8e69 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/doc.go @@ -0,0 +1,7 @@ +// Package shacrypt provides helpful abstractions for an implementation of SHA-crypt and implements +// github.com/go-crypt/crypt interfaces. +// +// See https://www.akkadia.org/drepper/SHA-crypt.html for specification details. +// +// This implementation is loaded by crypt.NewDefaultDecoder and crypt.NewDecoderAll. +package shacrypt diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/hasher.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/hasher.go new file mode 100644 index 000000000..6b8f9d11d --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/hasher.go @@ -0,0 +1,156 @@ +package shacrypt + +import ( + "fmt" + + xcrypt "github.com/go-crypt/x/crypt" + + "github.com/go-crypt/crypt/algorithm" + "github.com/go-crypt/crypt/internal/random" +) + +// New returns a *Hasher without any settings configured. This d to a SHA512 hash.Hash +// with 1000000 iterations. These settings can be overridden with the methods with the With prefix. +func New(opts ...Opt) (hasher *Hasher, err error) { + hasher = &Hasher{} + + if err = hasher.WithOptions(opts...); err != nil { + return nil, err + } + + if err = hasher.Validate(); err != nil { + return nil, err + } + + return hasher, nil +} + +// Hasher is a algorithm.Hash for SHA-crypt which can be initialized via shacrypt.New using a functional options pattern. +type Hasher struct { + variant Variant + + iterations, bytesSalt int + + d bool +} + +// NewSHA256 returns a *Hasher with the SHA256 hash.Hash which d to 1000000 iterations. These +// settings can be overridden with the methods with the With prefix. +func NewSHA256() (hasher *Hasher, err error) { + return New( + WithVariant(VariantSHA256), + WithIterations(VariantSHA256.DefaultIterations()), + ) +} + +// NewSHA512 returns a *Hasher with the SHA512 hash.Hash which d to 1000000 iterations. These +// settings can be overridden with the methods with the With prefix. +func NewSHA512() (hasher *Hasher, err error) { + return New( + WithVariant(VariantSHA512), + WithIterations(VariantSHA512.DefaultIterations()), + ) +} + +// WithOptions defines the options for this scrypt.Hasher. +func (h *Hasher) WithOptions(opts ...Opt) (err error) { + for _, opt := range opts { + if err = opt(h); err != nil { + return err + } + } + + return nil +} + +// Hash performs the hashing operation and returns either a shacrypt.Digest as a algorithm.Digest or an error. +func (h *Hasher) Hash(password string) (digest algorithm.Digest, err error) { + h.defaults() + + if digest, err = h.hash(password); err != nil { + return nil, fmt.Errorf(algorithm.ErrFmtHasherHash, AlgName, err) + } + + return digest, nil +} + +func (h *Hasher) hash(password string) (digest algorithm.Digest, err error) { + var salt []byte + + if salt, err = random.CharSetBytes(h.bytesSalt, SaltCharSet); err != nil { + return nil, fmt.Errorf("%w: %v", algorithm.ErrSaltReadRandomBytes, err) + } + + return h.hashWithSalt(password, salt) +} + +// HashWithSalt overloads the Hash method allowing the user to provide a salt. It's recommended instead to configure the +// salt size and let this be a random value generated using crypto/rand. +func (h *Hasher) HashWithSalt(password string, salt []byte) (digest algorithm.Digest, err error) { + h.defaults() + + if digest, err = h.hashWithSalt(password, salt); err != nil { + return nil, fmt.Errorf(algorithm.ErrFmtHasherHash, AlgName, err) + } + + return digest, nil +} + +func (h *Hasher) hashWithSalt(password string, salt []byte) (digest algorithm.Digest, err error) { + if s := len(salt); s > SaltLengthMax || s < SaltLengthMin { + return nil, fmt.Errorf("%w: salt bytes must have a length of between %d and %d but has a length of %d", algorithm.ErrSaltInvalid, SaltLengthMin, SaltLengthMax, len(salt)) + } + + d := &Digest{ + variant: h.variant, + iterations: h.iterations, + salt: salt, + } + + d.defaults() + + d.key = xcrypt.KeySHACrypt(d.variant.HashFunc(), []byte(password), d.salt, d.iterations) + + return d, nil +} + +// MustHash overloads the Hash method and panics if the error is not nil. It's recommended if you use this option to +// utilize the Validate method first or handle the panic appropriately. +func (h *Hasher) MustHash(password string) (digest algorithm.Digest) { + var err error + + if digest, err = h.Hash(password); err != nil { + panic(err) + } + + return digest +} + +// Validate checks the settings/parameters for this shacrypt.Hasher and returns an error. +func (h *Hasher) Validate() (err error) { + h.defaults() + + if err = h.validate(); err != nil { + return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, err) + } + + return nil +} + +func (h *Hasher) validate() (err error) { + h.defaults() + + return nil +} + +func (h *Hasher) defaults() { + if h.d { + return + } + + h.d = true + + if h.bytesSalt < SaltLengthMin { + h.bytesSalt = algorithm.SaltLengthDefault + } +} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/opts.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/opts.go new file mode 100644 index 000000000..3499882be --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/opts.go @@ -0,0 +1,98 @@ +package shacrypt + +import ( + "fmt" + + "github.com/go-crypt/crypt/algorithm" +) + +// Opt describes the functional option pattern for the shacrypt.Hasher. +type Opt func(h *Hasher) (err error) + +// WithVariant configures the shacrypt.Variant of the resulting shacrypt.Digest. +// Default is shacrypt.VariantSHA512. +func WithVariant(variant Variant) Opt { + return func(h *Hasher) (err error) { + switch variant { + case VariantNone: + return nil + case VariantSHA256, VariantSHA512: + h.variant = variant + + return nil + default: + return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf("%w: variant '%d' is invalid", algorithm.ErrParameterInvalid, variant)) + } + } +} + +// WithVariantName uses the variant name or identifier to configure the shacrypt.Variant of the resulting shacrypt.Digest. +// Default is shacrypt.VariantSHA512. +func WithVariantName(identifier string) Opt { + return func(h *Hasher) (err error) { + if identifier == "" { + return nil + } + + variant := NewVariant(identifier) + + if variant == VariantNone { + return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf("%w: variant identifier '%s' is invalid", algorithm.ErrParameterInvalid, identifier)) + } + + h.variant = variant + + return nil + } +} + +// WithSHA256 adjusts this Hasher to utilize the SHA256 hash.Hash. +func WithSHA256() Opt { + return func(h *Hasher) (err error) { + h.variant = VariantSHA256 + + return nil + } +} + +// WithSHA512 adjusts this Hasher to utilize the SHA512 hash.Hash. +func WithSHA512() Opt { + return func(h *Hasher) (err error) { + h.variant = VariantSHA512 + + return nil + } +} + +// WithIterations sets the iterations parameter of the resulting shacrypt.Digest. +// Minimum 1000, Maximum 999999999. Default is 1000000. +func WithIterations(iterations int) Opt { + return func(h *Hasher) (err error) { + if iterations < IterationsMin || iterations > IterationsMax { + return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf(algorithm.ErrFmtInvalidIntParameter, algorithm.ErrParameterInvalid, "iterations", IterationsMin, "", IterationsMax, iterations)) + } + + h.iterations = iterations + + return nil + } +} + +// WithRounds is an alias for shacrypt.WithIterations. +func WithRounds(rounds int) Opt { + return WithIterations(rounds) +} + +// WithSaltLength adjusts the salt size (in bytes) of the resulting shacrypt.Digest. +// Minimum 1, Maximum 16. Default is 16. +func WithSaltLength(bytes int) Opt { + return func(h *Hasher) (err error) { + if bytes < SaltLengthMin || bytes > SaltLengthMax { + return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf(algorithm.ErrFmtInvalidIntParameter, algorithm.ErrParameterInvalid, "salt length", SaltLengthMin, "", SaltLengthMax, bytes)) + } + + h.bytesSalt = bytes + + return nil + } +} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/variant.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/variant.go new file mode 100644 index 000000000..681ddc4ec --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/variant.go @@ -0,0 +1,92 @@ +package shacrypt + +import ( + "crypto/sha256" + "crypto/sha512" + + "github.com/go-crypt/crypt/algorithm" +) + +// NewVariant converts an identifier string to a shacrypt.Variant. +func NewVariant(identifier string) Variant { + switch identifier { + case AlgIdentifierSHA256, algorithm.DigestSHA256: + return VariantSHA256 + case AlgIdentifierSHA512, algorithm.DigestSHA512: + return VariantSHA512 + default: + return VariantSHA512 + } +} + +// Variant is a variant of the shacrypt.Digest. +type Variant int + +const ( + // VariantNone is a variant of the shacrypt.Digest which is unknown. + VariantNone Variant = iota + + // VariantSHA256 is a variant of the shacrypt.Digest which uses SHA-256. + VariantSHA256 + + // VariantSHA512 is a variant of the shacrypt.Digest which uses SHA-512. + VariantSHA512 +) + +// String implements the fmt.Stringer returning a string representation of the shacrypt.Variant. +func (v Variant) String() (identifier string) { + switch v { + case VariantSHA256: + return algorithm.DigestSHA256 + case VariantSHA512: + return algorithm.DigestSHA512 + default: + return + } +} + +// Prefix returns the shacrypt.Variant prefix identifier. +func (v Variant) Prefix() (prefix string) { + switch v { + case VariantSHA256: + return AlgIdentifierSHA256 + case VariantSHA512: + return AlgIdentifierSHA512 + default: + return AlgIdentifierSHA512 + } +} + +// Name returns the Variant name. +func (v Variant) Name() (s string) { + switch v { + case VariantSHA256: + return algorithm.DigestSHA256 + case VariantSHA512: + return algorithm.DigestSHA512 + default: + return algorithm.DigestSHA512 + } +} + +// HashFunc returns the internal HMAC HashFunc. +func (v Variant) HashFunc() algorithm.HashFunc { + switch v { + case VariantSHA256: + return sha256.New + case VariantSHA512: + return sha512.New + default: + return sha512.New + } +} + +// DefaultIterations returns the default iterations for the particular variant. +func (v Variant) DefaultIterations() int { + switch v { + case VariantSHA512: + return IterationsDefaultSHA512 + default: + return IterationsDefaultSHA256 + } +} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/types.go b/vendor/github.com/go-crypt/crypt/algorithm/types.go new file mode 100644 index 000000000..99fcb583a --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/algorithm/types.go @@ -0,0 +1,62 @@ +package algorithm + +import ( + "fmt" + "hash" +) + +// Hash is an interface which implements password hashing. +type Hash interface { + // Validate checks the hasher configuration to ensure it's valid. This should be used when the Hash is going to be + // reused and you should use it in conjunction with MustHash. + Validate() (err error) + + // Hash performs the hashing operation on a password and resets any relevant parameters such as a manually set salt. + // It then returns a Digest and error. + Hash(password string) (hashed Digest, err error) + + // HashWithSalt is an overload of Digest that also accepts a salt. + HashWithSalt(password string, salt []byte) (hashed Digest, err error) + + // MustHash overloads the Hash method and panics if the error is not nil. It's recommended if you use this method to + // utilize the Validate method first or handle the panic appropriately. + MustHash(password string) (hashed Digest) +} + +// Matcher is an interface used to match passwords. +type Matcher interface { + Match(password string) (match bool) + MatchBytes(passwordBytes []byte) (match bool) + MatchAdvanced(password string) (match bool, err error) + MatchBytesAdvanced(passwordBytes []byte) (match bool, err error) +} + +// Digest represents a hashed password. It's implemented by all hashed password results so that when we pass a +// stored hash into its relevant type we can verify the password against the hash. +type Digest interface { + fmt.Stringer + + Matcher + + Encode() (hash string) +} + +// DecodeFunc describes a function to decode an encoded digest into a algorithm.Digest. +type DecodeFunc func(encodedDigest string) (digest Digest, err error) + +// DecoderRegister describes an implementation that allows registering DecodeFunc's. +type DecoderRegister interface { + RegisterDecodeFunc(prefix string, decoder DecodeFunc) (err error) + RegisterDecodePrefix(prefix, identifier string) (err error) + + Decoder +} + +// Decoder is a representation of a implementation that performs generic decoding. Currently this is just intended for +// use by implementers. +type Decoder interface { + Decode(encodedDigest string) (digest Digest, err error) +} + +// HashFunc is a function which returns a hash.Hash. +type HashFunc func() hash.Hash diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/base64adapted.go b/vendor/github.com/go-crypt/crypt/internal/encoding/base64adapted.go new file mode 100644 index 000000000..55b84c115 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/internal/encoding/base64adapted.go @@ -0,0 +1,14 @@ +package encoding + +import ( + "encoding/base64" +) + +const ( + encodeBase64Adapted = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./" +) + +var ( + // Base64RawAdaptedEncoding is the adapted encoding for crypt purposes without padding. + Base64RawAdaptedEncoding = base64.NewEncoding(encodeBase64Adapted).WithPadding(base64.NoPadding) +) diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/const.go b/vendor/github.com/go-crypt/crypt/internal/encoding/const.go new file mode 100644 index 000000000..b0a3d56d3 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/internal/encoding/const.go @@ -0,0 +1,9 @@ +package encoding + +const ( + // Delimiter rune for all encodings. + Delimiter = rune('$') + + // DelimiterStr is the string variation of Delimiter. + DelimiterStr = string(Delimiter) +) diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/digest.go b/vendor/github.com/go-crypt/crypt/internal/encoding/digest.go new file mode 100644 index 000000000..f854968ab --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/internal/encoding/digest.go @@ -0,0 +1,10 @@ +package encoding + +import ( + "strings" +) + +// Split an encoded digest by the encoding.Delimiter. +func Split(encodedDigest string, n int) (parts []string) { + return strings.SplitN(encodedDigest, DelimiterStr, n) +} diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/doc.go b/vendor/github.com/go-crypt/crypt/internal/encoding/doc.go new file mode 100644 index 000000000..30a4f0be5 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/internal/encoding/doc.go @@ -0,0 +1,2 @@ +// Package encoding is an internal encoding helper package. +package encoding diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/parameters.go b/vendor/github.com/go-crypt/crypt/internal/encoding/parameters.go new file mode 100644 index 000000000..b7935be9a --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/internal/encoding/parameters.go @@ -0,0 +1,54 @@ +package encoding + +import ( + "fmt" + "strconv" + "strings" +) + +// Parameter is a key value pair. +type Parameter struct { + Key string + Value string +} + +// Int converts the Value to an int using strconv.Atoi. +func (p Parameter) Int() (int, error) { + return strconv.Atoi(p.Value) +} + +const ( + // ParameterDefaultItemSeparator is the default item separator. + ParameterDefaultItemSeparator = "," + + // ParameterDefaultKeyValueSeparator is the default key value separator. + ParameterDefaultKeyValueSeparator = "=" +) + +// DecodeParameterStr is an alias for DecodeParameterStrAdvanced using item separator and key value separator +// of ',' and '=' respectively. +func DecodeParameterStr(input string) (opts []Parameter, err error) { + return DecodeParameterStrAdvanced(input, ParameterDefaultItemSeparator, ParameterDefaultKeyValueSeparator) +} + +// DecodeParameterStrAdvanced decodes parameter strings into a []Parameter where sepItem separates each parameter, and sepKV separates the key and value. +func DecodeParameterStrAdvanced(input string, sepItem, sepKV string) (opts []Parameter, err error) { + if input == "" { + return nil, fmt.Errorf("empty strings can't be decoded to parameters") + } + + o := strings.Split(input, sepItem) + + opts = make([]Parameter, len(o)) + + for i, joined := range o { + kv := strings.SplitN(joined, sepKV, 2) + if len(kv) != 2 { + return nil, fmt.Errorf("parameter pair '%s' is not properly encoded: does not contain kv separator '%s'", joined, sepKV) + } + + opts[i] = Parameter{Key: kv[0], Value: kv[1]} + } + + return opts, nil +} diff --git a/vendor/github.com/go-crypt/crypt/internal/random/bytes.go b/vendor/github.com/go-crypt/crypt/internal/random/bytes.go new file mode 100644 index 000000000..20bc23535 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/internal/random/bytes.go @@ -0,0 +1,32 @@ +package random + +import ( + "crypto/rand" + "io" +) + +// Bytes returns random arbitrary bytes with a length of n. +func Bytes(n int) (bytes []byte, err error) { + bytes = make([]byte, n) + + if _, err = io.ReadFull(rand.Reader, bytes); err != nil { + return nil, err + } + + return bytes, nil +} + +// CharSetBytes returns random bytes with a length of n from the characters in the charset. +func CharSetBytes(n int, charset string) (bytes []byte, err error) { + bytes = make([]byte, n) + + if _, err = rand.Read(bytes); err != nil { + return nil, err + } + + for i, b := range bytes { + bytes[i] = charset[b%byte(len(charset))] + } + + return bytes, nil +} diff --git a/vendor/github.com/go-crypt/crypt/internal/random/doc.go b/vendor/github.com/go-crypt/crypt/internal/random/doc.go new file mode 100644 index 000000000..2bf9f24d3 --- /dev/null +++ b/vendor/github.com/go-crypt/crypt/internal/random/doc.go @@ -0,0 +1,2 @@ +// Package random is an internal helper package. +package random diff --git a/vendor/github.com/go-crypt/x/LICENSE b/vendor/github.com/go-crypt/x/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/vendor/github.com/go-crypt/x/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/go-crypt/x/base64/base64.go b/vendor/github.com/go-crypt/x/base64/base64.go new file mode 100644 index 000000000..2cb382a79 --- /dev/null +++ b/vendor/github.com/go-crypt/x/base64/base64.go @@ -0,0 +1,51 @@ +package base64 + +import ( + "encoding/base64" +) + +var AdaptedEncoding = base64.NewEncoding(encodeAdapted) + +// BcryptEncoding is the Bcrypt Base64 Alternative encoding. +var BcryptEncoding = base64.NewEncoding(bcryptB64Alphabet) + +// EncodeCrypt implements the linux crypt lib's B64 encoding. +func EncodeCrypt(src []byte) (dst []byte) { + if len(src) == 0 { + return nil + } + + dst = make([]byte, (len(src)*8+5)/6) + + idst, isrc := 0, 0 + + for isrc < len(src)/3*3 { + v := uint(src[isrc+2])<<16 | uint(src[isrc+1])<<8 | uint(src[isrc]) + dst[idst+0] = cryptB64Alphabet[v&0x3f] + dst[idst+1] = cryptB64Alphabet[v>>6&0x3f] + dst[idst+2] = cryptB64Alphabet[v>>12&0x3f] + dst[idst+3] = cryptB64Alphabet[v>>18] + idst += 4 + isrc += 3 + } + + remainder := len(src) - isrc + + if remainder == 0 { + return dst + } + + v := uint(src[isrc+0]) + if remainder == 2 { + v |= uint(src[isrc+1]) << 8 + } + + dst[idst+0] = cryptB64Alphabet[v&0x3f] + dst[idst+1] = cryptB64Alphabet[v>>6&0x3f] + + if remainder == 2 { + dst[idst+2] = cryptB64Alphabet[v>>12] + } + + return dst +} diff --git a/vendor/github.com/go-crypt/x/base64/const.go b/vendor/github.com/go-crypt/x/base64/const.go new file mode 100644 index 000000000..2e9923687 --- /dev/null +++ b/vendor/github.com/go-crypt/x/base64/const.go @@ -0,0 +1,7 @@ +package base64 + +const ( + cryptB64Alphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + bcryptB64Alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + encodeAdapted = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./" +) diff --git a/vendor/github.com/go-crypt/x/crypt/const.go b/vendor/github.com/go-crypt/x/crypt/const.go new file mode 100644 index 000000000..09153bfb7 --- /dev/null +++ b/vendor/github.com/go-crypt/x/crypt/const.go @@ -0,0 +1,223 @@ +package crypt + +var permuteTableMD5Crypt = [16]byte{ + 12, 6, 0, + 13, 7, 1, + 14, 8, 2, + 15, 9, 3, + 5, 10, 4, + 11, +} + +var permuteTableSHA1Crypt = [21]byte{ + 2, 1, 0, + 5, 4, 3, + 8, 7, 6, + 11, 10, 9, + 14, 13, 12, + 17, 16, 15, + 0, 19, 18, +} + +var permuteTableSHACryptSHA256 = [32]byte{ + 20, 10, 0, + 11, 1, 21, + 2, 22, 12, + 23, 13, 3, + 14, 4, 24, + 5, 25, 15, + 26, 16, 6, + 17, 7, 27, + 8, 28, 18, + 29, 19, 9, + 30, 31, +} + +var permuteTableSHACryptSHA512 = [64]byte{ + 42, 21, 0, + 1, 43, 22, + 23, 2, 44, + 45, 24, 3, + 4, 46, 25, + 26, 5, 47, + 48, 27, 6, + 7, 49, 28, + 29, 8, 50, + 51, 30, 9, + 10, 52, 31, + 32, 11, 53, + 54, 33, 12, + 13, 55, 34, + 35, 14, 56, + 57, 36, 15, + 16, 58, 37, + 38, 17, 59, + 60, 39, 18, + 19, 61, 40, + 41, 20, 62, + 63, +} + +// The following is the 1517 bytes of Hamlet III.ii which is public domain. This is used by Sun's MD5 Crypt function. +var magicTableMD5CryptSunHamlet = [1517]byte{ + 84, 111, 32, 98, 101, 44, 32, 111, 114, 32, 110, + 111, 116, 32, 116, 111, 32, 98, 101, 44, 45, + 45, 116, 104, 97, 116, 32, 105, 115, 32, 116, + 104, 101, 32, 113, 117, 101, 115, 116, 105, 111, + 110, 58, 45, 45, 10, 87, 104, 101, 116, 104, + 101, 114, 32, 39, 116, 105, 115, 32, 110, 111, + 98, 108, 101, 114, 32, 105, 110, 32, 116, 104, + 101, 32, 109, 105, 110, 100, 32, 116, 111, 32, + 115, 117, 102, 102, 101, 114, 10, 84, 104, 101, + 32, 115, 108, 105, 110, 103, 115, 32, 97, 110, + 100, 32, 97, 114, 114, 111, 119, 115, 32, 111, + 102, 32, 111, 117, 116, 114, 97, 103, 101, 111, + 117, 115, 32, 102, 111, 114, 116, 117, 110, 101, + 10, 79, 114, 32, 116, 111, 32, 116, 97, 107, + 101, 32, 97, 114, 109, 115, 32, 97, 103, 97, + 105, 110, 115, 116, 32, 97, 32, 115, 101, 97, + 32, 111, 102, 32, 116, 114, 111, 117, 98, 108, + 101, 115, 44, 10, 65, 110, 100, 32, 98, 121, + 32, 111, 112, 112, 111, 115, 105, 110, 103, 32, + 101, 110, 100, 32, 116, 104, 101, 109, 63, 45, + 45, 84, 111, 32, 100, 105, 101, 44, 45, 45, + 116, 111, 32, 115, 108, 101, 101, 112, 44, 45, + 45, 10, 78, 111, 32, 109, 111, 114, 101, 59, + 32, 97, 110, 100, 32, 98, 121, 32, 97, 32, + 115, 108, 101, 101, 112, 32, 116, 111, 32, 115, + 97, 121, 32, 119, 101, 32, 101, 110, 100, 10, + 84, 104, 101, 32, 104, 101, 97, 114, 116, 97, + 99, 104, 101, 44, 32, 97, 110, 100, 32, 116, + 104, 101, 32, 116, 104, 111, 117, 115, 97, 110, + 100, 32, 110, 97, 116, 117, 114, 97, 108, 32, + 115, 104, 111, 99, 107, 115, 10, 84, 104, 97, + 116, 32, 102, 108, 101, 115, 104, 32, 105, 115, + 32, 104, 101, 105, 114, 32, 116, 111, 44, 45, + 45, 39, 116, 105, 115, 32, 97, 32, 99, 111, + 110, 115, 117, 109, 109, 97, 116, 105, 111, 110, + 10, 68, 101, 118, 111, 117, 116, 108, 121, 32, + 116, 111, 32, 98, 101, 32, 119, 105, 115, 104, + 39, 100, 46, 32, 84, 111, 32, 100, 105, 101, + 44, 45, 45, 116, 111, 32, 115, 108, 101, 101, + 112, 59, 45, 45, 10, 84, 111, 32, 115, 108, + 101, 101, 112, 33, 32, 112, 101, 114, 99, 104, + 97, 110, 99, 101, 32, 116, 111, 32, 100, 114, + 101, 97, 109, 58, 45, 45, 97, 121, 44, 32, + 116, 104, 101, 114, 101, 39, 115, 32, 116, 104, + 101, 32, 114, 117, 98, 59, 10, 70, 111, 114, + 32, 105, 110, 32, 116, 104, 97, 116, 32, 115, + 108, 101, 101, 112, 32, 111, 102, 32, 100, 101, + 97, 116, 104, 32, 119, 104, 97, 116, 32, 100, + 114, 101, 97, 109, 115, 32, 109, 97, 121, 32, + 99, 111, 109, 101, 44, 10, 87, 104, 101, 110, + 32, 119, 101, 32, 104, 97, 118, 101, 32, 115, + 104, 117, 102, 102, 108, 101, 100, 32, 111, 102, + 102, 32, 116, 104, 105, 115, 32, 109, 111, 114, + 116, 97, 108, 32, 99, 111, 105, 108, 44, 10, + 77, 117, 115, 116, 32, 103, 105, 118, 101, 32, + 117, 115, 32, 112, 97, 117, 115, 101, 58, 32, + 116, 104, 101, 114, 101, 39, 115, 32, 116, 104, + 101, 32, 114, 101, 115, 112, 101, 99, 116, 10, + 84, 104, 97, 116, 32, 109, 97, 107, 101, 115, + 32, 99, 97, 108, 97, 109, 105, 116, 121, 32, + 111, 102, 32, 115, 111, 32, 108, 111, 110, 103, + 32, 108, 105, 102, 101, 59, 10, 70, 111, 114, + 32, 119, 104, 111, 32, 119, 111, 117, 108, 100, + 32, 98, 101, 97, 114, 32, 116, 104, 101, 32, + 119, 104, 105, 112, 115, 32, 97, 110, 100, 32, + 115, 99, 111, 114, 110, 115, 32, 111, 102, 32, + 116, 105, 109, 101, 44, 10, 84, 104, 101, 32, + 111, 112, 112, 114, 101, 115, 115, 111, 114, 39, + 115, 32, 119, 114, 111, 110, 103, 44, 32, 116, + 104, 101, 32, 112, 114, 111, 117, 100, 32, 109, + 97, 110, 39, 115, 32, 99, 111, 110, 116, 117, + 109, 101, 108, 121, 44, 10, 84, 104, 101, 32, + 112, 97, 110, 103, 115, 32, 111, 102, 32, 100, + 101, 115, 112, 105, 115, 39, 100, 32, 108, 111, + 118, 101, 44, 32, 116, 104, 101, 32, 108, 97, + 119, 39, 115, 32, 100, 101, 108, 97, 121, 44, + 10, 84, 104, 101, 32, 105, 110, 115, 111, 108, + 101, 110, 99, 101, 32, 111, 102, 32, 111, 102, + 102, 105, 99, 101, 44, 32, 97, 110, 100, 32, + 116, 104, 101, 32, 115, 112, 117, 114, 110, 115, + 10, 84, 104, 97, 116, 32, 112, 97, 116, 105, + 101, 110, 116, 32, 109, 101, 114, 105, 116, 32, + 111, 102, 32, 116, 104, 101, 32, 117, 110, 119, + 111, 114, 116, 104, 121, 32, 116, 97, 107, 101, + 115, 44, 10, 87, 104, 101, 110, 32, 104, 101, + 32, 104, 105, 109, 115, 101, 108, 102, 32, 109, + 105, 103, 104, 116, 32, 104, 105, 115, 32, 113, + 117, 105, 101, 116, 117, 115, 32, 109, 97, 107, + 101, 10, 87, 105, 116, 104, 32, 97, 32, 98, + 97, 114, 101, 32, 98, 111, 100, 107, 105, 110, + 63, 32, 119, 104, 111, 32, 119, 111, 117, 108, + 100, 32, 116, 104, 101, 115, 101, 32, 102, 97, + 114, 100, 101, 108, 115, 32, 98, 101, 97, 114, + 44, 10, 84, 111, 32, 103, 114, 117, 110, 116, + 32, 97, 110, 100, 32, 115, 119, 101, 97, 116, + 32, 117, 110, 100, 101, 114, 32, 97, 32, 119, + 101, 97, 114, 121, 32, 108, 105, 102, 101, 44, + 10, 66, 117, 116, 32, 116, 104, 97, 116, 32, + 116, 104, 101, 32, 100, 114, 101, 97, 100, 32, + 111, 102, 32, 115, 111, 109, 101, 116, 104, 105, + 110, 103, 32, 97, 102, 116, 101, 114, 32, 100, + 101, 97, 116, 104, 44, 45, 45, 10, 84, 104, + 101, 32, 117, 110, 100, 105, 115, 99, 111, 118, + 101, 114, 39, 100, 32, 99, 111, 117, 110, 116, + 114, 121, 44, 32, 102, 114, 111, 109, 32, 119, + 104, 111, 115, 101, 32, 98, 111, 117, 114, 110, + 10, 78, 111, 32, 116, 114, 97, 118, 101, 108, + 108, 101, 114, 32, 114, 101, 116, 117, 114, 110, + 115, 44, 45, 45, 112, 117, 122, 122, 108, 101, + 115, 32, 116, 104, 101, 32, 119, 105, 108, 108, + 44, 10, 65, 110, 100, 32, 109, 97, 107, 101, + 115, 32, 117, 115, 32, 114, 97, 116, 104, 101, + 114, 32, 98, 101, 97, 114, 32, 116, 104, 111, + 115, 101, 32, 105, 108, 108, 115, 32, 119, 101, + 32, 104, 97, 118, 101, 10, 84, 104, 97, 110, + 32, 102, 108, 121, 32, 116, 111, 32, 111, 116, + 104, 101, 114, 115, 32, 116, 104, 97, 116, 32, + 119, 101, 32, 107, 110, 111, 119, 32, 110, 111, + 116, 32, 111, 102, 63, 10, 84, 104, 117, 115, + 32, 99, 111, 110, 115, 99, 105, 101, 110, 99, + 101, 32, 100, 111, 101, 115, 32, 109, 97, 107, + 101, 32, 99, 111, 119, 97, 114, 100, 115, 32, + 111, 102, 32, 117, 115, 32, 97, 108, 108, 59, + 10, 65, 110, 100, 32, 116, 104, 117, 115, 32, + 116, 104, 101, 32, 110, 97, 116, 105, 118, 101, + 32, 104, 117, 101, 32, 111, 102, 32, 114, 101, + 115, 111, 108, 117, 116, 105, 111, 110, 10, 73, + 115, 32, 115, 105, 99, 107, 108, 105, 101, 100, + 32, 111, 39, 101, 114, 32, 119, 105, 116, 104, + 32, 116, 104, 101, 32, 112, 97, 108, 101, 32, + 99, 97, 115, 116, 32, 111, 102, 32, 116, 104, + 111, 117, 103, 104, 116, 59, 10, 65, 110, 100, + 32, 101, 110, 116, 101, 114, 112, 114, 105, 115, + 101, 115, 32, 111, 102, 32, 103, 114, 101, 97, + 116, 32, 112, 105, 116, 104, 32, 97, 110, 100, + 32, 109, 111, 109, 101, 110, 116, 44, 10, 87, + 105, 116, 104, 32, 116, 104, 105, 115, 32, 114, + 101, 103, 97, 114, 100, 44, 32, 116, 104, 101, + 105, 114, 32, 99, 117, 114, 114, 101, 110, 116, + 115, 32, 116, 117, 114, 110, 32, 97, 119, 114, + 121, 44, 10, 65, 110, 100, 32, 108, 111, 115, + 101, 32, 116, 104, 101, 32, 110, 97, 109, 101, + 32, 111, 102, 32, 97, 99, 116, 105, 111, 110, + 46, 45, 45, 83, 111, 102, 116, 32, 121, 111, + 117, 32, 110, 111, 119, 33, 10, 84, 104, 101, + 32, 102, 97, 105, 114, 32, 79, 112, 104, 101, + 108, 105, 97, 33, 45, 45, 78, 121, 109, 112, + 104, 44, 32, 105, 110, 32, 116, 104, 121, 32, + 111, 114, 105, 115, 111, 110, 115, 10, 66, 101, + 32, 97, 108, 108, 32, 109, 121, 32, 115, 105, + 110, 115, 32, 114, 101, 109, 101, 109, 98, 101, + 114, 39, 100, 46, 10, 0, +} + +var ( + prefixMD5Crypt = []byte("$1$") + prefixSHA1Crypt = []byte("$sha1$") + prefixSunMD5Crypt = []byte("$md5$") + prefixSunMD5CryptRounds = []byte("$md5,rounds=") + sepCrypt = []byte("$") +) diff --git a/vendor/github.com/go-crypt/x/crypt/crypt.go b/vendor/github.com/go-crypt/x/crypt/crypt.go new file mode 100644 index 000000000..3bd2cdb13 --- /dev/null +++ b/vendor/github.com/go-crypt/x/crypt/crypt.go @@ -0,0 +1,300 @@ +package crypt + +import ( + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "hash" + "strconv" +) + +// KeySHACrypt calculates the shacrypt SHA256/SHA512 key given an appropriate hash.Hash, password, salt, and number of rounds. +func KeySHACrypt(hashFunc func() hash.Hash, password, salt []byte, rounds int) []byte { + // Step 1. + digest := hashFunc() + + size := digest.Size() + + switch size { + case sha1.Size: + return KeySHA1Crypt(password, salt, uint32(rounds)) + case sha256.Size, sha512.Size: + break + default: + return nil + } + + length := len(password) + + // Step 2. + digest.Write(password) + + // Step 3. + digest.Write(salt) + + // Step 4. + digestB := hashFunc() + + // Step 5. + digestB.Write(password) + + // Step 6. + digestB.Write(salt) + + // Step 7. + digestB.Write(password) + + // Step 8. + sumB := digestB.Sum(nil) + digestB.Reset() + digestB = nil + + // Step 9 and 10: + digest.Write(repeat(sumB, length)) + + // Step 11. + for i := length; i > 0; i >>= 1 { + if even(i) { + digest.Write(password) + } else { + digest.Write(sumB) + } + } + + clean(sumB) + sumB = nil + + // Step 12. + sumA := digest.Sum(nil) + digest.Reset() + + // Step 13-14. + for i := 0; i < length; i++ { + digest.Write(password) + } + + // Step 15. + sumDP := digest.Sum(nil) + digest.Reset() + + // Step 16. + seqP := repeat(sumDP, length) + sumDP = nil + + // Step 17-18. + for i := 0; i < 16+int(sumA[0]); i++ { + digest.Write(salt) + } + + // Step 19. + sumDS := digest.Sum(nil) + digest.Reset() + + // Step 20. + seqS := repeat(sumDS, len(salt)) + + // Step 21. + for i := 0; i < rounds; i++ { + digest.Reset() + + // Step 21 Sub-Step B and C. + if i&1 != 0 { + // Step 21 Sub-Step B. + digest.Write(seqP) + } else { + // Step 21 Sub-Step C. + digest.Write(sumA) + } + + // Step 21 Sub-Step D. + if i%3 != 0 { + digest.Write(seqS) + } + + // Step 21 Sub-Step E. + if i%7 != 0 { + digest.Write(seqP) + } + + // Step 21 Sub-Step F and G. + if i&1 != 0 { + // Step 21 Sub-Step F. + digest.Write(sumA) + } else { + // Step 21 Sub-Step G. + digest.Write(seqP) + } + + // Sub-Step H. + copy(sumA, digest.Sum(nil)) + } + + digest.Reset() + digest = nil + + seqP, seqS = nil, nil + + switch size { + case sha256.Size: + // Step 22 Sub Step E. + return permute(sumA, permuteTableSHACryptSHA256[:]) + case sha512.Size: + // Step 22 Sub Step E. + return permute(sumA, permuteTableSHACryptSHA512[:]) + } + + return nil +} + +// KeySHA1Crypt calculates the sha1crypt key given a password, salt, and number of rounds. +func KeySHA1Crypt(password, salt []byte, rounds uint32) []byte { + digest := hmac.New(sha1.New, password) + digest.Write(salt) + digest.Write(prefixSHA1Crypt) + digest.Write([]byte(strconv.FormatUint(uint64(rounds), 10))) + + sumA := digest.Sum(nil) + + if rounds == 0 { + return permute(sumA, permuteTableSHA1Crypt[:]) + } + + for rounds--; rounds > 0; rounds-- { + digest.Reset() + + digest.Write(sumA) + + copy(sumA, digest.Sum(nil)) + } + + return permute(sumA, permuteTableSHA1Crypt[:]) +} + +// KeyMD5Crypt calculates the md5crypt key given a password and salt. +func KeyMD5Crypt(password, salt []byte) []byte { + length := len(password) + + digest := md5.New() + + digest.Write(password) + digest.Write(salt) + digest.Write(password) + + sumB := digest.Sum(nil) + + digest.Reset() + + digest.Write(password) + digest.Write(prefixMD5Crypt) + digest.Write(salt) + digest.Write(repeat(sumB, length)) + + clean(sumB) + + for i := length; i > 0; i >>= 1 { + if even(i) { + digest.Write(password[0:1]) + } else { + digest.Write([]byte{0}) + } + } + + sumA := digest.Sum(nil) + + for i := 0; i < 1000; i++ { + digest.Reset() + + if even(i) { + digest.Write(sumA) + } else { + digest.Write(password) + } + + if i%3 != 0 { + digest.Write(salt) + } + + if i%7 != 0 { + digest.Write(password) + } + + if i&1 == 0 { + digest.Write(password) + } else { + digest.Write(sumA) + } + + copy(sumA, digest.Sum(nil)) + } + + return permute(sumA, permuteTableMD5Crypt[:]) +} + +// KeyMD5CryptSun calculates the md5crypt (Sun Version) key given a password, salt, and number rounds. +func KeyMD5CryptSun(password, salt []byte, rounds uint32) []byte { + digest := md5.New() + + digest.Write(password) + + if rounds == 0 { + digest.Write(prefixSunMD5Crypt) + digest.Write(salt) + digest.Write(sepCrypt) + } else { + digest.Write(prefixSunMD5CryptRounds) + digest.Write([]byte(strconv.FormatUint(uint64(rounds), 10))) + digest.Write(sepCrypt) + digest.Write(salt) + digest.Write(sepCrypt) + } + + sumA := digest.Sum(nil) + + iterations := uint32(rounds + 4096) + + bit := func(off uint32) uint32 { + off %= 128 + if (sumA[off/8] & (0x01 << (off % 8))) != 0 { + return 1 + } + + return 0 + } + + var ind7 [md5.Size]byte + + for i := uint32(0); i < iterations; i++ { + digest.Reset() + + digest.Write(sumA) + + for j := 0; j < md5.Size; j++ { + off := (j + 3) % 16 + ind4 := (sumA[j] >> (sumA[off] % 5)) & 0x0F + sh7 := (sumA[off] >> (sumA[j] % 8)) & 0x01 + ind7[j] = (sumA[ind4] >> sh7) & 0x7F + } + + var indA, indB uint32 + + for j := uint(0); j < 8; j++ { + indA |= bit(uint32(ind7[j])) << j + indB |= bit(uint32(ind7[j+8])) << j + } + + indA = (indA >> bit(i)) & 0x7F + indB = (indB >> bit(i+64)) & 0x7F + + if bit(indA)^bit(indB) == 1 { + digest.Write(magicTableMD5CryptSunHamlet[:]) + } + + digest.Write([]byte(strconv.FormatUint(uint64(i), 10))) + + copy(sumA, digest.Sum(nil)) + } + + return permute(sumA, permuteTableMD5Crypt[:]) +} diff --git a/vendor/github.com/go-crypt/x/crypt/util.go b/vendor/github.com/go-crypt/x/crypt/util.go new file mode 100644 index 000000000..c5e6fa447 --- /dev/null +++ b/vendor/github.com/go-crypt/x/crypt/util.go @@ -0,0 +1,54 @@ +package crypt + +import ( + b64 "github.com/go-crypt/x/base64" +) + +func permute(sum, table []byte) []byte { + size := len(table) + + key := make([]byte, size) + + for i := 0; i < size; i++ { + key[i] = sum[table[i]] + } + + return b64.EncodeCrypt(key) +} + +func even(i int) bool { + return i%2 == 0 +} + +var ( + cleanBytes = make([]byte, 64) +) + +func clean(b []byte) { + l := len(b) + + for ; l > 64; l -= 64 { + copy(b[l-64:l], cleanBytes) + } + + if l > 0 { + copy(b[0:l], cleanBytes[0:l]) + } +} + +func repeat(input []byte, length int) []byte { + var ( + seq = make([]byte, length) + unit = len(input) + ) + + j := length / unit * unit + for i := 0; i < j; i += unit { + copy(seq[i:length], input) + } + if j < length { + copy(seq[j:length], input[0:length-j]) + } + + return seq +} diff --git a/vendor/github.com/gogs/chardet/2022.go b/vendor/github.com/gogs/chardet/2022.go new file mode 100644 index 000000000..e667225e5 --- /dev/null +++ b/vendor/github.com/gogs/chardet/2022.go @@ -0,0 +1,102 @@ +package chardet + +import ( + "bytes" +) + +type recognizer2022 struct { + charset string + escapes [][]byte +} + +func (r *recognizer2022) Match(input *recognizerInput) (output recognizerOutput) { + return recognizerOutput{ + Charset: r.charset, + Confidence: r.matchConfidence(input.input), + } +} + +func (r *recognizer2022) matchConfidence(input []byte) int { + var hits, misses, shifts int +input: + for i := 0; i < len(input); i++ { + c := input[i] + if c == 0x1B { + for _, esc := range r.escapes { + if bytes.HasPrefix(input[i+1:], esc) { + hits++ + i += len(esc) + continue input + } + } + misses++ + } else if c == 0x0E || c == 0x0F { + shifts++ + } + } + if hits == 0 { + return 0 + } + quality := (100*hits - 100*misses) / (hits + misses) + if hits+shifts < 5 { + quality -= (5 - (hits + shifts)) * 10 + } + if quality < 0 { + quality = 0 + } + return quality +} + +var escapeSequences_2022JP = [][]byte{ + {0x24, 0x28, 0x43}, // KS X 1001:1992 + {0x24, 0x28, 0x44}, // JIS X 212-1990 + {0x24, 0x40}, // JIS C 6226-1978 + {0x24, 0x41}, // GB 2312-80 + {0x24, 0x42}, // JIS X 208-1983 + {0x26, 0x40}, // JIS X 208 1990, 1997 + {0x28, 0x42}, // ASCII + {0x28, 0x48}, // JIS-Roman + {0x28, 0x49}, // Half-width katakana + {0x28, 0x4a}, // JIS-Roman + {0x2e, 0x41}, // ISO 8859-1 + {0x2e, 0x46}, // ISO 8859-7 +} + +var escapeSequences_2022KR = [][]byte{ + {0x24, 0x29, 0x43}, +} + +var escapeSequences_2022CN = [][]byte{ + {0x24, 0x29, 0x41}, // GB 2312-80 + {0x24, 0x29, 0x47}, // CNS 11643-1992 Plane 1 + {0x24, 0x2A, 0x48}, // CNS 11643-1992 Plane 2 + {0x24, 0x29, 0x45}, // ISO-IR-165 + {0x24, 0x2B, 0x49}, // CNS 11643-1992 Plane 3 + {0x24, 0x2B, 0x4A}, // CNS 11643-1992 Plane 4 + {0x24, 0x2B, 0x4B}, // CNS 11643-1992 Plane 5 + {0x24, 0x2B, 0x4C}, // CNS 11643-1992 Plane 6 + {0x24, 0x2B, 0x4D}, // CNS 11643-1992 Plane 7 + {0x4e}, // SS2 + {0x4f}, // SS3 +} + +func newRecognizer_2022JP() *recognizer2022 { + return &recognizer2022{ + "ISO-2022-JP", + escapeSequences_2022JP, + } +} + +func newRecognizer_2022KR() *recognizer2022 { + return &recognizer2022{ + "ISO-2022-KR", + escapeSequences_2022KR, + } +} + +func newRecognizer_2022CN() *recognizer2022 { + return &recognizer2022{ + "ISO-2022-CN", + escapeSequences_2022CN, + } +} diff --git a/vendor/github.com/gogs/chardet/AUTHORS b/vendor/github.com/gogs/chardet/AUTHORS new file mode 100644 index 000000000..842d0216d --- /dev/null +++ b/vendor/github.com/gogs/chardet/AUTHORS @@ -0,0 +1 @@ +Sheng Yu (yusheng dot sjtu at gmail dot com) diff --git a/vendor/github.com/gogs/chardet/LICENSE b/vendor/github.com/gogs/chardet/LICENSE new file mode 100644 index 000000000..35ee796b9 --- /dev/null +++ b/vendor/github.com/gogs/chardet/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 chardet Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Partial of the Software is derived from ICU project. See icu-license.html for +license of the derivative portions. diff --git a/vendor/github.com/gogs/chardet/README.md b/vendor/github.com/gogs/chardet/README.md new file mode 100644 index 000000000..f5c7cc208 --- /dev/null +++ b/vendor/github.com/gogs/chardet/README.md @@ -0,0 +1,12 @@ +# chardet + +chardet is library to automatically detect +[charset](http://en.wikipedia.org/wiki/Character_encoding) of texts for [Go +programming language](http://golang.org/). It's based on the algorithm and data +in [ICU](http://icu-project.org/)'s implementation. + +The project was created by [saintfish](http://github.com/saintfish/chardet). In January 2015 it was forked by the gogits project in order to incorporate bugfixes and new features. + +## Documentation and Usage + +See [pkgdoc](http://godoc.org/github.com/gogs/chardet) diff --git a/vendor/github.com/gogs/chardet/detector.go b/vendor/github.com/gogs/chardet/detector.go new file mode 100644 index 000000000..027e1c37b --- /dev/null +++ b/vendor/github.com/gogs/chardet/detector.go @@ -0,0 +1,147 @@ +// Package chardet ports character set detection from ICU. +package chardet + +import ( + "errors" + "sort" +) + +// Result contains all the information that charset detector gives. +type Result struct { + // IANA name of the detected charset. + Charset string + // IANA name of the detected language. It may be empty for some charsets. + Language string + // Confidence of the Result. Scale from 1 to 100. The bigger, the more confident. + Confidence int +} + +// Detector implements charset detection. +type Detector struct { + recognizers []recognizer + stripTag bool +} + +// List of charset recognizers +var recognizers = []recognizer{ + newRecognizer_utf8(), + newRecognizer_utf16be(), + newRecognizer_utf16le(), + newRecognizer_utf32be(), + newRecognizer_utf32le(), + newRecognizer_8859_1_en(), + newRecognizer_8859_1_da(), + newRecognizer_8859_1_de(), + newRecognizer_8859_1_es(), + newRecognizer_8859_1_fr(), + newRecognizer_8859_1_it(), + newRecognizer_8859_1_nl(), + newRecognizer_8859_1_no(), + newRecognizer_8859_1_pt(), + newRecognizer_8859_1_sv(), + newRecognizer_8859_2_cs(), + newRecognizer_8859_2_hu(), + newRecognizer_8859_2_pl(), + newRecognizer_8859_2_ro(), + newRecognizer_8859_5_ru(), + newRecognizer_8859_6_ar(), + newRecognizer_8859_7_el(), + newRecognizer_8859_8_I_he(), + newRecognizer_8859_8_he(), + newRecognizer_windows_1251(), + newRecognizer_windows_1256(), + newRecognizer_KOI8_R(), + newRecognizer_8859_9_tr(), + + newRecognizer_sjis(), + newRecognizer_gb_18030(), + newRecognizer_euc_jp(), + newRecognizer_euc_kr(), + newRecognizer_big5(), + + newRecognizer_2022JP(), + newRecognizer_2022KR(), + newRecognizer_2022CN(), + + newRecognizer_IBM424_he_rtl(), + newRecognizer_IBM424_he_ltr(), + newRecognizer_IBM420_ar_rtl(), + newRecognizer_IBM420_ar_ltr(), +} + +// NewTextDetector creates a Detector for plain text. +func NewTextDetector() *Detector { + return &Detector{recognizers, false} +} + +// NewHtmlDetector creates a Detector for Html. +func NewHtmlDetector() *Detector { + return &Detector{recognizers, true} +} + +var ( + NotDetectedError = errors.New("Charset not detected.") +) + +// DetectBest returns the Result with highest Confidence. +func (d *Detector) DetectBest(b []byte) (r *Result, err error) { + input := newRecognizerInput(b, d.stripTag) + outputChan := make(chan recognizerOutput) + for _, r := range d.recognizers { + go matchHelper(r, input, outputChan) + } + var output Result + for i := 0; i < len(d.recognizers); i++ { + o := <-outputChan + if output.Confidence < o.Confidence { + output = Result(o) + } + } + if output.Confidence == 0 { + return nil, NotDetectedError + } + return &output, nil +} + +// DetectAll returns all Results which have non-zero Confidence. The Results are sorted by Confidence in descending order. +func (d *Detector) DetectAll(b []byte) ([]Result, error) { + input := newRecognizerInput(b, d.stripTag) + outputChan := make(chan recognizerOutput) + for _, r := range d.recognizers { + go matchHelper(r, input, outputChan) + } + outputs := make(recognizerOutputs, 0, len(d.recognizers)) + for i := 0; i < len(d.recognizers); i++ { + o := <-outputChan + if o.Confidence > 0 { + outputs = append(outputs, o) + } + } + if len(outputs) == 0 { + return nil, NotDetectedError + } + + sort.Sort(outputs) + dedupOutputs := make([]Result, 0, len(outputs)) + foundCharsets := make(map[string]struct{}, len(outputs)) + for _, o := range outputs { + if _, found := foundCharsets[o.Charset]; !found { + dedupOutputs = append(dedupOutputs, Result(o)) + foundCharsets[o.Charset] = struct{}{} + } + } + if len(dedupOutputs) == 0 { + return nil, NotDetectedError + } + return dedupOutputs, nil +} + +func matchHelper(r recognizer, input *recognizerInput, outputChan chan<- recognizerOutput) { + outputChan <- r.Match(input) +} + +type recognizerOutputs []recognizerOutput + +func (r recognizerOutputs) Len() int { return len(r) } +func (r recognizerOutputs) Less(i, j int) bool { return r[i].Confidence > r[j].Confidence } +func (r recognizerOutputs) Swap(i, j int) { r[i], r[j] = r[j], r[i] } diff --git a/vendor/github.com/gogs/chardet/icu-license.html b/vendor/github.com/gogs/chardet/icu-license.html new file mode 100644 index 000000000..d078d0575 --- /dev/null +++ b/vendor/github.com/gogs/chardet/icu-license.html @@ -0,0 +1,51 @@ + + + + +ICU License - ICU 1.8.1 and later + + + +

ICU License - ICU 1.8.1 and later

+ +

COPYRIGHT AND PERMISSION NOTICE

+ +

+Copyright (c) 1995-2012 International Business Machines Corporation and others +

+

+All rights reserved. +

+

+Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies +of the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. +

+

+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, +OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. +

+

+Except as contained in this notice, the name of a copyright holder shall not be +used in advertising or otherwise to promote the sale, use or other dealings in +this Software without prior written authorization of the copyright holder. +

+ +
+

+All trademarks and registered trademarks mentioned herein are the property of their respective owners. +

+ + diff --git a/vendor/github.com/gogs/chardet/multi_byte.go b/vendor/github.com/gogs/chardet/multi_byte.go new file mode 100644 index 000000000..b5cdf3d6d --- /dev/null +++ b/vendor/github.com/gogs/chardet/multi_byte.go @@ -0,0 +1,345 @@ +package chardet + +import ( + "errors" + "math" +) + +type recognizerMultiByte struct { + charset string + language string + decoder charDecoder + commonChars []uint16 +} + +type charDecoder interface { + DecodeOneChar([]byte) (c uint16, remain []byte, err error) +} + +func (r *recognizerMultiByte) Match(input *recognizerInput) (output recognizerOutput) { + return recognizerOutput{ + Charset: r.charset, + Language: r.language, + Confidence: r.matchConfidence(input), + } +} + +func (r *recognizerMultiByte) matchConfidence(input *recognizerInput) int { + raw := input.raw + var c uint16 + var err error + var totalCharCount, badCharCount, singleByteCharCount, doubleByteCharCount, commonCharCount int + for c, raw, err = r.decoder.DecodeOneChar(raw); len(raw) > 0; c, raw, err = r.decoder.DecodeOneChar(raw) { + totalCharCount++ + if err != nil { + badCharCount++ + } else if c <= 0xFF { + singleByteCharCount++ + } else { + doubleByteCharCount++ + if r.commonChars != nil && binarySearch(r.commonChars, c) { + commonCharCount++ + } + } + if badCharCount >= 2 && badCharCount*5 >= doubleByteCharCount { + return 0 + } + } + + if doubleByteCharCount <= 10 && badCharCount == 0 { + if doubleByteCharCount == 0 && totalCharCount < 10 { + return 0 + } else { + return 10 + } + } + + if doubleByteCharCount < 20*badCharCount { + return 0 + } + if r.commonChars == nil { + confidence := 30 + doubleByteCharCount - 20*badCharCount + if confidence > 100 { + confidence = 100 + } + return confidence + } + maxVal := math.Log(float64(doubleByteCharCount) / 4) + scaleFactor := 90 / maxVal + confidence := int(math.Log(float64(commonCharCount)+1)*scaleFactor + 10) + if confidence > 100 { + confidence = 100 + } + if confidence < 0 { + confidence = 0 + } + return confidence +} + +func binarySearch(l []uint16, c uint16) bool { + start := 0 + end := len(l) - 1 + for start <= end { + mid := (start + end) / 2 + if c == l[mid] { + return true + } else if c < l[mid] { + end = mid - 1 + } else { + start = mid + 1 + } + } + return false +} + +var eobError = errors.New("End of input buffer") +var badCharError = errors.New("Decode a bad char") + +type charDecoder_sjis struct { +} + +func (charDecoder_sjis) DecodeOneChar(input []byte) (c uint16, remain []byte, err error) { + if len(input) == 0 { + return 0, nil, eobError + } + first := input[0] + c = uint16(first) + remain = input[1:] + if first <= 0x7F || (first > 0xA0 && first <= 0xDF) { + return + } + if len(remain) == 0 { + return c, remain, badCharError + } + second := remain[0] + remain = remain[1:] + c = c<<8 | uint16(second) + if (second >= 0x40 && second <= 0x7F) || (second >= 0x80 && second <= 0xFE) { + } else { + err = badCharError + } + return +} + +var commonChars_sjis = []uint16{ + 0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0, + 0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5, + 0x82b7, 0x82bd, 0x82be, 0x82c1, 0x82c4, 0x82c5, 0x82c6, 0x82c8, 0x82c9, 0x82cc, + 0x82cd, 0x82dc, 0x82e0, 0x82e7, 0x82e8, 0x82e9, 0x82ea, 0x82f0, 0x82f1, 0x8341, + 0x8343, 0x834e, 0x834f, 0x8358, 0x835e, 0x8362, 0x8367, 0x8375, 0x8376, 0x8389, + 0x838a, 0x838b, 0x838d, 0x8393, 0x8e96, 0x93fa, 0x95aa, +} + +func newRecognizer_sjis() *recognizerMultiByte { + return &recognizerMultiByte{ + "Shift_JIS", + "ja", + charDecoder_sjis{}, + commonChars_sjis, + } +} + +type charDecoder_euc struct { +} + +func (charDecoder_euc) DecodeOneChar(input []byte) (c uint16, remain []byte, err error) { + if len(input) == 0 { + return 0, nil, eobError + } + first := input[0] + remain = input[1:] + c = uint16(first) + if first <= 0x8D { + return uint16(first), remain, nil + } + if len(remain) == 0 { + return 0, nil, eobError + } + second := remain[0] + remain = remain[1:] + c = c<<8 | uint16(second) + if first >= 0xA1 && first <= 0xFE { + if second < 0xA1 { + err = badCharError + } + return + } + if first == 0x8E { + if second < 0xA1 { + err = badCharError + } + return + } + if first == 0x8F { + if len(remain) == 0 { + return 0, nil, eobError + } + third := remain[0] + remain = remain[1:] + c = c<<0 | uint16(third) + if third < 0xa1 { + err = badCharError + } + } + return +} + +var commonChars_euc_jp = []uint16{ + 0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2, + 0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3, + 0xa4b5, 0xa4b7, 0xa4b9, 0xa4bb, 0xa4bd, 0xa4bf, 0xa4c0, 0xa4c1, 0xa4c3, 0xa4c4, + 0xa4c6, 0xa4c7, 0xa4c8, 0xa4c9, 0xa4ca, 0xa4cb, 0xa4ce, 0xa4cf, 0xa4d0, 0xa4de, + 0xa4df, 0xa4e1, 0xa4e2, 0xa4e4, 0xa4e8, 0xa4e9, 0xa4ea, 0xa4eb, 0xa4ec, 0xa4ef, + 0xa4f2, 0xa4f3, 0xa5a2, 0xa5a3, 0xa5a4, 0xa5a6, 0xa5a7, 0xa5aa, 0xa5ad, 0xa5af, + 0xa5b0, 0xa5b3, 0xa5b5, 0xa5b7, 0xa5b8, 0xa5b9, 0xa5bf, 0xa5c3, 0xa5c6, 0xa5c7, + 0xa5c8, 0xa5c9, 0xa5cb, 0xa5d0, 0xa5d5, 0xa5d6, 0xa5d7, 0xa5de, 0xa5e0, 0xa5e1, + 0xa5e5, 0xa5e9, 0xa5ea, 0xa5eb, 0xa5ec, 0xa5ed, 0xa5f3, 0xb8a9, 0xb9d4, 0xbaee, + 0xbbc8, 0xbef0, 0xbfb7, 0xc4ea, 0xc6fc, 0xc7bd, 0xcab8, 0xcaf3, 0xcbdc, 0xcdd1, +} + +var commonChars_euc_kr = []uint16{ + 0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc, + 0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9, + 0xb4eb, 0xb5a5, 0xb5b5, 0xb5bf, 0xb5c7, 0xb5e9, 0xb6f3, 0xb7af, 0xb7c2, 0xb7ce, + 0xb8a6, 0xb8ae, 0xb8b6, 0xb8b8, 0xb8bb, 0xb8e9, 0xb9ab, 0xb9ae, 0xb9cc, 0xb9ce, + 0xb9fd, 0xbab8, 0xbace, 0xbad0, 0xbaf1, 0xbbe7, 0xbbf3, 0xbbfd, 0xbcad, 0xbcba, + 0xbcd2, 0xbcf6, 0xbdba, 0xbdc0, 0xbdc3, 0xbdc5, 0xbec6, 0xbec8, 0xbedf, 0xbeee, + 0xbef8, 0xbefa, 0xbfa1, 0xbfa9, 0xbfc0, 0xbfe4, 0xbfeb, 0xbfec, 0xbff8, 0xc0a7, + 0xc0af, 0xc0b8, 0xc0ba, 0xc0bb, 0xc0bd, 0xc0c7, 0xc0cc, 0xc0ce, 0xc0cf, 0xc0d6, + 0xc0da, 0xc0e5, 0xc0fb, 0xc0fc, 0xc1a4, 0xc1a6, 0xc1b6, 0xc1d6, 0xc1df, 0xc1f6, + 0xc1f8, 0xc4a1, 0xc5cd, 0xc6ae, 0xc7cf, 0xc7d1, 0xc7d2, 0xc7d8, 0xc7e5, 0xc8ad, +} + +func newRecognizer_euc_jp() *recognizerMultiByte { + return &recognizerMultiByte{ + "EUC-JP", + "ja", + charDecoder_euc{}, + commonChars_euc_jp, + } +} + +func newRecognizer_euc_kr() *recognizerMultiByte { + return &recognizerMultiByte{ + "EUC-KR", + "ko", + charDecoder_euc{}, + commonChars_euc_kr, + } +} + +type charDecoder_big5 struct { +} + +func (charDecoder_big5) DecodeOneChar(input []byte) (c uint16, remain []byte, err error) { + if len(input) == 0 { + return 0, nil, eobError + } + first := input[0] + remain = input[1:] + c = uint16(first) + if first <= 0x7F || first == 0xFF { + return + } + if len(remain) == 0 { + return c, nil, eobError + } + second := remain[0] + remain = remain[1:] + c = c<<8 | uint16(second) + if second < 0x40 || second == 0x7F || second == 0xFF { + err = badCharError + } + return +} + +var commonChars_big5 = []uint16{ + 0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446, + 0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3, + 0xa4a4, 0xa4a7, 0xa4c1, 0xa4ce, 0xa4d1, 0xa4df, 0xa4e8, 0xa4fd, 0xa540, 0xa548, + 0xa558, 0xa569, 0xa5cd, 0xa5e7, 0xa657, 0xa661, 0xa662, 0xa668, 0xa670, 0xa6a8, + 0xa6b3, 0xa6b9, 0xa6d3, 0xa6db, 0xa6e6, 0xa6f2, 0xa740, 0xa751, 0xa759, 0xa7da, + 0xa8a3, 0xa8a5, 0xa8ad, 0xa8d1, 0xa8d3, 0xa8e4, 0xa8fc, 0xa9c0, 0xa9d2, 0xa9f3, + 0xaa6b, 0xaaba, 0xaabe, 0xaacc, 0xaafc, 0xac47, 0xac4f, 0xacb0, 0xacd2, 0xad59, + 0xaec9, 0xafe0, 0xb0ea, 0xb16f, 0xb2b3, 0xb2c4, 0xb36f, 0xb44c, 0xb44e, 0xb54c, + 0xb5a5, 0xb5bd, 0xb5d0, 0xb5d8, 0xb671, 0xb7ed, 0xb867, 0xb944, 0xbad8, 0xbb44, + 0xbba1, 0xbdd1, 0xc2c4, 0xc3b9, 0xc440, 0xc45f, +} + +func newRecognizer_big5() *recognizerMultiByte { + return &recognizerMultiByte{ + "Big5", + "zh", + charDecoder_big5{}, + commonChars_big5, + } +} + +type charDecoder_gb_18030 struct { +} + +func (charDecoder_gb_18030) DecodeOneChar(input []byte) (c uint16, remain []byte, err error) { + if len(input) == 0 { + return 0, nil, eobError + } + first := input[0] + remain = input[1:] + c = uint16(first) + if first <= 0x80 { + return + } + if len(remain) == 0 { + return 0, nil, eobError + } + second := remain[0] + remain = remain[1:] + c = c<<8 | uint16(second) + if first >= 0x81 && first <= 0xFE { + if (second >= 0x40 && second <= 0x7E) || (second >= 0x80 && second <= 0xFE) { + return + } + + if second >= 0x30 && second <= 0x39 { + if len(remain) == 0 { + return 0, nil, eobError + } + third := remain[0] + remain = remain[1:] + if third >= 0x81 && third <= 0xFE { + if len(remain) == 0 { + return 0, nil, eobError + } + fourth := remain[0] + remain = remain[1:] + if fourth >= 0x30 && fourth <= 0x39 { + c = c<<16 | uint16(third)<<8 | uint16(fourth) + return + } + } + } + err = badCharError + } + return +} + +var commonChars_gb_18030 = []uint16{ + 0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac, + 0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4, + 0xb5e3, 0xb6af, 0xb6d4, 0xb6e0, 0xb7a2, 0xb7a8, 0xb7bd, 0xb7d6, 0xb7dd, 0xb8b4, + 0xb8df, 0xb8f6, 0xb9ab, 0xb9c9, 0xb9d8, 0xb9fa, 0xb9fd, 0xbacd, 0xbba7, 0xbbd6, + 0xbbe1, 0xbbfa, 0xbcbc, 0xbcdb, 0xbcfe, 0xbdcc, 0xbecd, 0xbedd, 0xbfb4, 0xbfc6, + 0xbfc9, 0xc0b4, 0xc0ed, 0xc1cb, 0xc2db, 0xc3c7, 0xc4dc, 0xc4ea, 0xc5cc, 0xc6f7, + 0xc7f8, 0xc8ab, 0xc8cb, 0xc8d5, 0xc8e7, 0xc9cf, 0xc9fa, 0xcab1, 0xcab5, 0xcac7, + 0xcad0, 0xcad6, 0xcaf5, 0xcafd, 0xccec, 0xcdf8, 0xceaa, 0xcec4, 0xced2, 0xcee5, + 0xcfb5, 0xcfc2, 0xcfd6, 0xd0c2, 0xd0c5, 0xd0d0, 0xd0d4, 0xd1a7, 0xd2aa, 0xd2b2, + 0xd2b5, 0xd2bb, 0xd2d4, 0xd3c3, 0xd3d0, 0xd3fd, 0xd4c2, 0xd4da, 0xd5e2, 0xd6d0, +} + +func newRecognizer_gb_18030() *recognizerMultiByte { + return &recognizerMultiByte{ + "GB18030", + "zh", + charDecoder_gb_18030{}, + commonChars_gb_18030, + } +} diff --git a/vendor/github.com/gogs/chardet/recognizer.go b/vendor/github.com/gogs/chardet/recognizer.go new file mode 100644 index 000000000..1bf8461c3 --- /dev/null +++ b/vendor/github.com/gogs/chardet/recognizer.go @@ -0,0 +1,83 @@ +package chardet + +type recognizer interface { + Match(*recognizerInput) recognizerOutput +} + +type recognizerOutput Result + +type recognizerInput struct { + raw []byte + input []byte + tagStripped bool + byteStats []int + hasC1Bytes bool +} + +func newRecognizerInput(raw []byte, stripTag bool) *recognizerInput { + input, stripped := mayStripInput(raw, stripTag) + byteStats := computeByteStats(input) + return &recognizerInput{ + raw: raw, + input: input, + tagStripped: stripped, + byteStats: byteStats, + hasC1Bytes: computeHasC1Bytes(byteStats), + } +} + +func mayStripInput(raw []byte, stripTag bool) (out []byte, stripped bool) { + const inputBufferSize = 8192 + out = make([]byte, 0, inputBufferSize) + var badTags, openTags int32 + var inMarkup bool = false + stripped = false + if stripTag { + stripped = true + for _, c := range raw { + if c == '<' { + if inMarkup { + badTags += 1 + } + inMarkup = true + openTags += 1 + } + if !inMarkup { + out = append(out, c) + if len(out) >= inputBufferSize { + break + } + } + if c == '>' { + inMarkup = false + } + } + } + if openTags < 5 || openTags/5 < badTags || (len(out) < 100 && len(raw) > 600) { + limit := len(raw) + if limit > inputBufferSize { + limit = inputBufferSize + } + out = make([]byte, limit) + copy(out, raw[:limit]) + stripped = false + } + return +} + +func computeByteStats(input []byte) []int { + r := make([]int, 256) + for _, c := range input { + r[c] += 1 + } + return r +} + +func computeHasC1Bytes(byteStats []int) bool { + for _, count := range byteStats[0x80 : 0x9F+1] { + if count > 0 { + return true + } + } + return false +} diff --git a/vendor/github.com/gogs/chardet/single_byte.go b/vendor/github.com/gogs/chardet/single_byte.go new file mode 100644 index 000000000..a7ce39bc2 --- /dev/null +++ b/vendor/github.com/gogs/chardet/single_byte.go @@ -0,0 +1,882 @@ +package chardet + +// Recognizer for single byte charset family +type recognizerSingleByte struct { + charset string + hasC1ByteCharset string + language string + charMap *[256]byte + ngram *[64]uint32 +} + +func (r *recognizerSingleByte) Match(input *recognizerInput) recognizerOutput { + var charset string = r.charset + if input.hasC1Bytes && len(r.hasC1ByteCharset) > 0 { + charset = r.hasC1ByteCharset + } + return recognizerOutput{ + Charset: charset, + Language: r.language, + Confidence: r.parseNgram(input.input), + } +} + +type ngramState struct { + ngram uint32 + ignoreSpace bool + ngramCount, ngramHit uint32 + table *[64]uint32 +} + +func newNgramState(table *[64]uint32) *ngramState { + return &ngramState{ + ngram: 0, + ignoreSpace: false, + ngramCount: 0, + ngramHit: 0, + table: table, + } +} + +func (s *ngramState) AddByte(b byte) { + const ngramMask = 0xFFFFFF + if !(b == 0x20 && s.ignoreSpace) { + s.ngram = ((s.ngram << 8) | uint32(b)) & ngramMask + s.ignoreSpace = (s.ngram == 0x20) + s.ngramCount++ + if s.lookup() { + s.ngramHit++ + } + } + s.ignoreSpace = (b == 0x20) +} + +func (s *ngramState) HitRate() float32 { + if s.ngramCount == 0 { + return 0 + } + return float32(s.ngramHit) / float32(s.ngramCount) +} + +func (s *ngramState) lookup() bool { + var index int + if s.table[index+32] <= s.ngram { + index += 32 + } + if s.table[index+16] <= s.ngram { + index += 16 + } + if s.table[index+8] <= s.ngram { + index += 8 + } + if s.table[index+4] <= s.ngram { + index += 4 + } + if s.table[index+2] <= s.ngram { + index += 2 + } + if s.table[index+1] <= s.ngram { + index += 1 + } + if s.table[index] > s.ngram { + index -= 1 + } + if index < 0 || s.table[index] != s.ngram { + return false + } + return true +} + +func (r *recognizerSingleByte) parseNgram(input []byte) int { + state := newNgramState(r.ngram) + for _, inChar := range input { + c := r.charMap[inChar] + if c != 0 { + state.AddByte(c) + } + } + state.AddByte(0x20) + rate := state.HitRate() + if rate > 0.33 { + return 98 + } + return int(rate * 300) +} + +var charMap_8859_1 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0xAA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0xB5, 0x20, 0x20, + 0x20, 0x20, 0xBA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, +} + +var ngrams_8859_1_en = [64]uint32{ + 0x206120, 0x20616E, 0x206265, 0x20636F, 0x20666F, 0x206861, 0x206865, 0x20696E, 0x206D61, 0x206F66, 0x207072, 0x207265, 0x207361, 0x207374, 0x207468, 0x20746F, + 0x207768, 0x616964, 0x616C20, 0x616E20, 0x616E64, 0x617320, 0x617420, 0x617465, 0x617469, 0x642061, 0x642074, 0x652061, 0x652073, 0x652074, 0x656420, 0x656E74, + 0x657220, 0x657320, 0x666F72, 0x686174, 0x686520, 0x686572, 0x696420, 0x696E20, 0x696E67, 0x696F6E, 0x697320, 0x6E2061, 0x6E2074, 0x6E6420, 0x6E6720, 0x6E7420, + 0x6F6620, 0x6F6E20, 0x6F7220, 0x726520, 0x727320, 0x732061, 0x732074, 0x736169, 0x737420, 0x742074, 0x746572, 0x746861, 0x746865, 0x74696F, 0x746F20, 0x747320, +} + +var ngrams_8859_1_da = [64]uint32{ + 0x206166, 0x206174, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207369, 0x207374, 0x207469, 0x207669, 0x616620, + 0x616E20, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646572, 0x646574, 0x652073, 0x656420, 0x656465, 0x656E20, 0x656E64, 0x657220, 0x657265, 0x657320, + 0x657420, 0x666F72, 0x676520, 0x67656E, 0x676572, 0x696765, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6572, 0x6C6967, 0x6C6C65, 0x6D6564, 0x6E6465, 0x6E6520, + 0x6E6720, 0x6E6765, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722064, 0x722065, 0x722073, 0x726520, 0x737465, 0x742073, 0x746520, 0x746572, 0x74696C, 0x766572, +} + +var ngrams_8859_1_de = [64]uint32{ + 0x20616E, 0x206175, 0x206265, 0x206461, 0x206465, 0x206469, 0x206569, 0x206765, 0x206861, 0x20696E, 0x206D69, 0x207363, 0x207365, 0x20756E, 0x207665, 0x20766F, + 0x207765, 0x207A75, 0x626572, 0x636820, 0x636865, 0x636874, 0x646173, 0x64656E, 0x646572, 0x646965, 0x652064, 0x652073, 0x65696E, 0x656974, 0x656E20, 0x657220, + 0x657320, 0x67656E, 0x68656E, 0x687420, 0x696368, 0x696520, 0x696E20, 0x696E65, 0x697420, 0x6C6963, 0x6C6C65, 0x6E2061, 0x6E2064, 0x6E2073, 0x6E6420, 0x6E6465, + 0x6E6520, 0x6E6720, 0x6E6765, 0x6E7465, 0x722064, 0x726465, 0x726569, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x756E64, 0x756E67, 0x766572, +} + +var ngrams_8859_1_es = [64]uint32{ + 0x206120, 0x206361, 0x20636F, 0x206465, 0x20656C, 0x20656E, 0x206573, 0x20696E, 0x206C61, 0x206C6F, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, + 0x20756E, 0x207920, 0x612063, 0x612064, 0x612065, 0x61206C, 0x612070, 0x616369, 0x61646F, 0x616C20, 0x617220, 0x617320, 0x6369F3, 0x636F6E, 0x646520, 0x64656C, + 0x646F20, 0x652064, 0x652065, 0x65206C, 0x656C20, 0x656E20, 0x656E74, 0x657320, 0x657374, 0x69656E, 0x69F36E, 0x6C6120, 0x6C6F73, 0x6E2065, 0x6E7465, 0x6F2064, + 0x6F2065, 0x6F6E20, 0x6F7220, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732064, 0x732065, 0x732070, 0x736520, 0x746520, 0x746F20, 0x756520, 0xF36E20, +} + +var ngrams_8859_1_fr = [64]uint32{ + 0x206175, 0x20636F, 0x206461, 0x206465, 0x206475, 0x20656E, 0x206574, 0x206C61, 0x206C65, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207365, 0x20736F, 0x20756E, + 0x20E020, 0x616E74, 0x617469, 0x636520, 0x636F6E, 0x646520, 0x646573, 0x647520, 0x652061, 0x652063, 0x652064, 0x652065, 0x65206C, 0x652070, 0x652073, 0x656E20, + 0x656E74, 0x657220, 0x657320, 0x657420, 0x657572, 0x696F6E, 0x697320, 0x697420, 0x6C6120, 0x6C6520, 0x6C6573, 0x6D656E, 0x6E2064, 0x6E6520, 0x6E7320, 0x6E7420, + 0x6F6E20, 0x6F6E74, 0x6F7572, 0x717565, 0x72206C, 0x726520, 0x732061, 0x732064, 0x732065, 0x73206C, 0x732070, 0x742064, 0x746520, 0x74696F, 0x756520, 0x757220, +} + +var ngrams_8859_1_it = [64]uint32{ + 0x20616C, 0x206368, 0x20636F, 0x206465, 0x206469, 0x206520, 0x20696C, 0x20696E, 0x206C61, 0x207065, 0x207072, 0x20756E, 0x612063, 0x612064, 0x612070, 0x612073, + 0x61746F, 0x636865, 0x636F6E, 0x64656C, 0x646920, 0x652061, 0x652063, 0x652064, 0x652069, 0x65206C, 0x652070, 0x652073, 0x656C20, 0x656C6C, 0x656E74, 0x657220, + 0x686520, 0x692061, 0x692063, 0x692064, 0x692073, 0x696120, 0x696C20, 0x696E20, 0x696F6E, 0x6C6120, 0x6C6520, 0x6C6920, 0x6C6C61, 0x6E6520, 0x6E6920, 0x6E6F20, + 0x6E7465, 0x6F2061, 0x6F2064, 0x6F2069, 0x6F2073, 0x6F6E20, 0x6F6E65, 0x706572, 0x726120, 0x726520, 0x736920, 0x746120, 0x746520, 0x746920, 0x746F20, 0x7A696F, +} + +var ngrams_8859_1_nl = [64]uint32{ + 0x20616C, 0x206265, 0x206461, 0x206465, 0x206469, 0x206565, 0x20656E, 0x206765, 0x206865, 0x20696E, 0x206D61, 0x206D65, 0x206F70, 0x207465, 0x207661, 0x207665, + 0x20766F, 0x207765, 0x207A69, 0x61616E, 0x616172, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x636874, 0x646520, 0x64656E, 0x646572, 0x652062, 0x652076, 0x65656E, + 0x656572, 0x656E20, 0x657220, 0x657273, 0x657420, 0x67656E, 0x686574, 0x696520, 0x696E20, 0x696E67, 0x697320, 0x6E2062, 0x6E2064, 0x6E2065, 0x6E2068, 0x6E206F, + 0x6E2076, 0x6E6465, 0x6E6720, 0x6F6E64, 0x6F6F72, 0x6F7020, 0x6F7220, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x76616E, 0x766572, 0x766F6F, +} + +var ngrams_8859_1_no = [64]uint32{ + 0x206174, 0x206176, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207365, 0x20736B, 0x20736F, 0x207374, 0x207469, + 0x207669, 0x20E520, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646574, 0x652073, 0x656420, 0x656E20, 0x656E65, 0x657220, 0x657265, 0x657420, 0x657474, + 0x666F72, 0x67656E, 0x696B6B, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6520, 0x6C6C65, 0x6D6564, 0x6D656E, 0x6E2073, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E6E65, + 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722073, 0x726520, 0x736F6D, 0x737465, 0x742073, 0x746520, 0x74656E, 0x746572, 0x74696C, 0x747420, 0x747465, 0x766572, +} + +var ngrams_8859_1_pt = [64]uint32{ + 0x206120, 0x20636F, 0x206461, 0x206465, 0x20646F, 0x206520, 0x206573, 0x206D61, 0x206E6F, 0x206F20, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, + 0x20756D, 0x612061, 0x612063, 0x612064, 0x612070, 0x616465, 0x61646F, 0x616C20, 0x617220, 0x617261, 0x617320, 0x636F6D, 0x636F6E, 0x646120, 0x646520, 0x646F20, + 0x646F73, 0x652061, 0x652064, 0x656D20, 0x656E74, 0x657320, 0x657374, 0x696120, 0x696361, 0x6D656E, 0x6E7465, 0x6E746F, 0x6F2061, 0x6F2063, 0x6F2064, 0x6F2065, + 0x6F2070, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732061, 0x732064, 0x732065, 0x732070, 0x737461, 0x746520, 0x746F20, 0x756520, 0xE36F20, 0xE7E36F, +} + +var ngrams_8859_1_sv = [64]uint32{ + 0x206174, 0x206176, 0x206465, 0x20656E, 0x2066F6, 0x206861, 0x206920, 0x20696E, 0x206B6F, 0x206D65, 0x206F63, 0x2070E5, 0x20736B, 0x20736F, 0x207374, 0x207469, + 0x207661, 0x207669, 0x20E472, 0x616465, 0x616E20, 0x616E64, 0x617220, 0x617474, 0x636820, 0x646520, 0x64656E, 0x646572, 0x646574, 0x656420, 0x656E20, 0x657220, + 0x657420, 0x66F672, 0x67656E, 0x696C6C, 0x696E67, 0x6B6120, 0x6C6C20, 0x6D6564, 0x6E2073, 0x6E6120, 0x6E6465, 0x6E6720, 0x6E6765, 0x6E696E, 0x6F6368, 0x6F6D20, + 0x6F6E20, 0x70E520, 0x722061, 0x722073, 0x726120, 0x736B61, 0x736F6D, 0x742073, 0x746120, 0x746520, 0x746572, 0x74696C, 0x747420, 0x766172, 0xE47220, 0xF67220, +} + +func newRecognizer_8859_1(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-1", + hasC1ByteCharset: "windows-1252", + language: language, + charMap: &charMap_8859_1, + ngram: ngram, + } +} + +func newRecognizer_8859_1_en() *recognizerSingleByte { + return newRecognizer_8859_1("en", &ngrams_8859_1_en) +} +func newRecognizer_8859_1_da() *recognizerSingleByte { + return newRecognizer_8859_1("da", &ngrams_8859_1_da) +} +func newRecognizer_8859_1_de() *recognizerSingleByte { + return newRecognizer_8859_1("de", &ngrams_8859_1_de) +} +func newRecognizer_8859_1_es() *recognizerSingleByte { + return newRecognizer_8859_1("es", &ngrams_8859_1_es) +} +func newRecognizer_8859_1_fr() *recognizerSingleByte { + return newRecognizer_8859_1("fr", &ngrams_8859_1_fr) +} +func newRecognizer_8859_1_it() *recognizerSingleByte { + return newRecognizer_8859_1("it", &ngrams_8859_1_it) +} +func newRecognizer_8859_1_nl() *recognizerSingleByte { + return newRecognizer_8859_1("nl", &ngrams_8859_1_nl) +} +func newRecognizer_8859_1_no() *recognizerSingleByte { + return newRecognizer_8859_1("no", &ngrams_8859_1_no) +} +func newRecognizer_8859_1_pt() *recognizerSingleByte { + return newRecognizer_8859_1("pt", &ngrams_8859_1_pt) +} +func newRecognizer_8859_1_sv() *recognizerSingleByte { + return newRecognizer_8859_1("sv", &ngrams_8859_1_sv) +} + +var charMap_8859_2 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0xB1, 0x20, 0xB3, 0x20, 0xB5, 0xB6, 0x20, + 0x20, 0xB9, 0xBA, 0xBB, 0xBC, 0x20, 0xBE, 0xBF, + 0x20, 0xB1, 0x20, 0xB3, 0x20, 0xB5, 0xB6, 0xB7, + 0x20, 0xB9, 0xBA, 0xBB, 0xBC, 0x20, 0xBE, 0xBF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0x20, +} + +var ngrams_8859_2_cs = [64]uint32{ + 0x206120, 0x206279, 0x20646F, 0x206A65, 0x206E61, 0x206E65, 0x206F20, 0x206F64, 0x20706F, 0x207072, 0x2070F8, 0x20726F, 0x207365, 0x20736F, 0x207374, 0x20746F, + 0x207620, 0x207679, 0x207A61, 0x612070, 0x636520, 0x636820, 0x652070, 0x652073, 0x652076, 0x656D20, 0x656EED, 0x686F20, 0x686F64, 0x697374, 0x6A6520, 0x6B7465, + 0x6C6520, 0x6C6920, 0x6E6120, 0x6EE920, 0x6EEC20, 0x6EED20, 0x6F2070, 0x6F646E, 0x6F6A69, 0x6F7374, 0x6F7520, 0x6F7661, 0x706F64, 0x706F6A, 0x70726F, 0x70F865, + 0x736520, 0x736F75, 0x737461, 0x737469, 0x73746E, 0x746572, 0x746EED, 0x746F20, 0x752070, 0xBE6520, 0xE16EED, 0xE9686F, 0xED2070, 0xED2073, 0xED6D20, 0xF86564, +} + +var ngrams_8859_2_hu = [64]uint32{ + 0x206120, 0x20617A, 0x206265, 0x206567, 0x20656C, 0x206665, 0x206861, 0x20686F, 0x206973, 0x206B65, 0x206B69, 0x206BF6, 0x206C65, 0x206D61, 0x206D65, 0x206D69, + 0x206E65, 0x20737A, 0x207465, 0x20E973, 0x612061, 0x61206B, 0x61206D, 0x612073, 0x616B20, 0x616E20, 0x617A20, 0x62616E, 0x62656E, 0x656779, 0x656B20, 0x656C20, + 0x656C65, 0x656D20, 0x656E20, 0x657265, 0x657420, 0x657465, 0x657474, 0x677920, 0x686F67, 0x696E74, 0x697320, 0x6B2061, 0x6BF67A, 0x6D6567, 0x6D696E, 0x6E2061, + 0x6E616B, 0x6E656B, 0x6E656D, 0x6E7420, 0x6F6779, 0x732061, 0x737A65, 0x737A74, 0x737AE1, 0x73E967, 0x742061, 0x747420, 0x74E173, 0x7A6572, 0xE16E20, 0xE97320, +} + +var ngrams_8859_2_pl = [64]uint32{ + 0x20637A, 0x20646F, 0x206920, 0x206A65, 0x206B6F, 0x206D61, 0x206D69, 0x206E61, 0x206E69, 0x206F64, 0x20706F, 0x207072, 0x207369, 0x207720, 0x207769, 0x207779, + 0x207A20, 0x207A61, 0x612070, 0x612077, 0x616E69, 0x636820, 0x637A65, 0x637A79, 0x646F20, 0x647A69, 0x652070, 0x652073, 0x652077, 0x65207A, 0x65676F, 0x656A20, + 0x656D20, 0x656E69, 0x676F20, 0x696120, 0x696520, 0x69656A, 0x6B6120, 0x6B6920, 0x6B6965, 0x6D6965, 0x6E6120, 0x6E6961, 0x6E6965, 0x6F2070, 0x6F7761, 0x6F7769, + 0x706F6C, 0x707261, 0x70726F, 0x70727A, 0x727A65, 0x727A79, 0x7369EA, 0x736B69, 0x737461, 0x776965, 0x796368, 0x796D20, 0x7A6520, 0x7A6965, 0x7A7920, 0xF37720, +} + +var ngrams_8859_2_ro = [64]uint32{ + 0x206120, 0x206163, 0x206361, 0x206365, 0x20636F, 0x206375, 0x206465, 0x206469, 0x206C61, 0x206D61, 0x207065, 0x207072, 0x207365, 0x2073E3, 0x20756E, 0x20BA69, + 0x20EE6E, 0x612063, 0x612064, 0x617265, 0x617420, 0x617465, 0x617520, 0x636172, 0x636F6E, 0x637520, 0x63E320, 0x646520, 0x652061, 0x652063, 0x652064, 0x652070, + 0x652073, 0x656120, 0x656920, 0x656C65, 0x656E74, 0x657374, 0x692061, 0x692063, 0x692064, 0x692070, 0x696520, 0x696920, 0x696E20, 0x6C6120, 0x6C6520, 0x6C6F72, + 0x6C7569, 0x6E6520, 0x6E7472, 0x6F7220, 0x70656E, 0x726520, 0x726561, 0x727520, 0x73E320, 0x746520, 0x747275, 0x74E320, 0x756920, 0x756C20, 0xBA6920, 0xEE6E20, +} + +func newRecognizer_8859_2(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-2", + hasC1ByteCharset: "windows-1250", + language: language, + charMap: &charMap_8859_2, + ngram: ngram, + } +} + +func newRecognizer_8859_2_cs() *recognizerSingleByte { + return newRecognizer_8859_2("cs", &ngrams_8859_2_cs) +} +func newRecognizer_8859_2_hu() *recognizerSingleByte { + return newRecognizer_8859_2("hu", &ngrams_8859_2_hu) +} +func newRecognizer_8859_2_pl() *recognizerSingleByte { + return newRecognizer_8859_2("pl", &ngrams_8859_2_pl) +} +func newRecognizer_8859_2_ro() *recognizerSingleByte { + return newRecognizer_8859_2("ro", &ngrams_8859_2_ro) +} + +var charMap_8859_5 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x20, 0xFE, 0xFF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0x20, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x20, 0xFE, 0xFF, +} + +var ngrams_8859_5_ru = [64]uint32{ + 0x20D220, 0x20D2DE, 0x20D4DE, 0x20D7D0, 0x20D820, 0x20DAD0, 0x20DADE, 0x20DDD0, 0x20DDD5, 0x20DED1, 0x20DFDE, 0x20DFE0, 0x20E0D0, 0x20E1DE, 0x20E1E2, 0x20E2DE, + 0x20E7E2, 0x20EDE2, 0xD0DDD8, 0xD0E2EC, 0xD3DE20, 0xD5DBEC, 0xD5DDD8, 0xD5E1E2, 0xD5E220, 0xD820DF, 0xD8D520, 0xD8D820, 0xD8EF20, 0xDBD5DD, 0xDBD820, 0xDBECDD, + 0xDDD020, 0xDDD520, 0xDDD8D5, 0xDDD8EF, 0xDDDE20, 0xDDDED2, 0xDE20D2, 0xDE20DF, 0xDE20E1, 0xDED220, 0xDED2D0, 0xDED3DE, 0xDED920, 0xDEDBEC, 0xDEDC20, 0xDEE1E2, + 0xDFDEDB, 0xDFE0D5, 0xDFE0D8, 0xDFE0DE, 0xE0D0D2, 0xE0D5D4, 0xE1E2D0, 0xE1E2D2, 0xE1E2D8, 0xE1EF20, 0xE2D5DB, 0xE2DE20, 0xE2DEE0, 0xE2EC20, 0xE7E2DE, 0xEBE520, +} + +func newRecognizer_8859_5(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-5", + language: language, + charMap: &charMap_8859_5, + ngram: ngram, + } +} + +func newRecognizer_8859_5_ru() *recognizerSingleByte { + return newRecognizer_8859_5("ru", &ngrams_8859_5_ru) +} + +var charMap_8859_6 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, +} + +var ngrams_8859_6_ar = [64]uint32{ + 0x20C7E4, 0x20C7E6, 0x20C8C7, 0x20D9E4, 0x20E1EA, 0x20E4E4, 0x20E5E6, 0x20E8C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E420, 0xC7E4C3, 0xC7E4C7, 0xC7E4C8, + 0xC7E4CA, 0xC7E4CC, 0xC7E4CD, 0xC7E4CF, 0xC7E4D3, 0xC7E4D9, 0xC7E4E2, 0xC7E4E5, 0xC7E4E8, 0xC7E4EA, 0xC7E520, 0xC7E620, 0xC7E6CA, 0xC820C7, 0xC920C7, 0xC920E1, + 0xC920E4, 0xC920E5, 0xC920E8, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xD920C7, 0xD9E4E9, 0xE1EA20, 0xE420C7, 0xE4C920, 0xE4E920, 0xE4EA20, + 0xE520C7, 0xE5C720, 0xE5C920, 0xE5E620, 0xE620C7, 0xE720C7, 0xE7C720, 0xE8C7E4, 0xE8E620, 0xE920C7, 0xEA20C7, 0xEA20E5, 0xEA20E8, 0xEAC920, 0xEAD120, 0xEAE620, +} + +func newRecognizer_8859_6(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-6", + language: language, + charMap: &charMap_8859_6, + ngram: ngram, + } +} + +func newRecognizer_8859_6_ar() *recognizerSingleByte { + return newRecognizer_8859_6("ar", &ngrams_8859_6_ar) +} + +var charMap_8859_7 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0xA1, 0xA2, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xDC, 0x20, + 0xDD, 0xDE, 0xDF, 0x20, 0xFC, 0x20, 0xFD, 0xFE, + 0xC0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0x20, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0x20, +} + +var ngrams_8859_7_el = [64]uint32{ + 0x20E1ED, 0x20E1F0, 0x20E3E9, 0x20E4E9, 0x20E5F0, 0x20E720, 0x20EAE1, 0x20ECE5, 0x20EDE1, 0x20EF20, 0x20F0E1, 0x20F0EF, 0x20F0F1, 0x20F3F4, 0x20F3F5, 0x20F4E7, + 0x20F4EF, 0xDFE120, 0xE120E1, 0xE120F4, 0xE1E920, 0xE1ED20, 0xE1F0FC, 0xE1F220, 0xE3E9E1, 0xE5E920, 0xE5F220, 0xE720F4, 0xE7ED20, 0xE7F220, 0xE920F4, 0xE9E120, + 0xE9EADE, 0xE9F220, 0xEAE1E9, 0xEAE1F4, 0xECE520, 0xED20E1, 0xED20E5, 0xED20F0, 0xEDE120, 0xEFF220, 0xEFF520, 0xF0EFF5, 0xF0F1EF, 0xF0FC20, 0xF220E1, 0xF220E5, + 0xF220EA, 0xF220F0, 0xF220F4, 0xF3E520, 0xF3E720, 0xF3F4EF, 0xF4E120, 0xF4E1E9, 0xF4E7ED, 0xF4E7F2, 0xF4E9EA, 0xF4EF20, 0xF4EFF5, 0xF4F9ED, 0xF9ED20, 0xFEED20, +} + +func newRecognizer_8859_7(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-7", + hasC1ByteCharset: "windows-1253", + language: language, + charMap: &charMap_8859_7, + ngram: ngram, + } +} + +func newRecognizer_8859_7_el() *recognizerSingleByte { + return newRecognizer_8859_7("el", &ngrams_8859_7_el) +} + +var charMap_8859_8 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0xB5, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0x20, 0x20, 0x20, 0x20, 0x20, +} + +var ngrams_8859_8_I_he = [64]uint32{ + 0x20E0E5, 0x20E0E7, 0x20E0E9, 0x20E0FA, 0x20E1E9, 0x20E1EE, 0x20E4E0, 0x20E4E5, 0x20E4E9, 0x20E4EE, 0x20E4F2, 0x20E4F9, 0x20E4FA, 0x20ECE0, 0x20ECE4, 0x20EEE0, + 0x20F2EC, 0x20F9EC, 0xE0FA20, 0xE420E0, 0xE420E1, 0xE420E4, 0xE420EC, 0xE420EE, 0xE420F9, 0xE4E5E0, 0xE5E020, 0xE5ED20, 0xE5EF20, 0xE5F820, 0xE5FA20, 0xE920E4, + 0xE9E420, 0xE9E5FA, 0xE9E9ED, 0xE9ED20, 0xE9EF20, 0xE9F820, 0xE9FA20, 0xEC20E0, 0xEC20E4, 0xECE020, 0xECE420, 0xED20E0, 0xED20E1, 0xED20E4, 0xED20EC, 0xED20EE, + 0xED20F9, 0xEEE420, 0xEF20E4, 0xF0E420, 0xF0E920, 0xF0E9ED, 0xF2EC20, 0xF820E4, 0xF8E9ED, 0xF9EC20, 0xFA20E0, 0xFA20E1, 0xFA20E4, 0xFA20EC, 0xFA20EE, 0xFA20F9, +} + +var ngrams_8859_8_he = [64]uint32{ + 0x20E0E5, 0x20E0EC, 0x20E4E9, 0x20E4EC, 0x20E4EE, 0x20E4F0, 0x20E9F0, 0x20ECF2, 0x20ECF9, 0x20EDE5, 0x20EDE9, 0x20EFE5, 0x20EFE9, 0x20F8E5, 0x20F8E9, 0x20FAE0, + 0x20FAE5, 0x20FAE9, 0xE020E4, 0xE020EC, 0xE020ED, 0xE020FA, 0xE0E420, 0xE0E5E4, 0xE0EC20, 0xE0EE20, 0xE120E4, 0xE120ED, 0xE120FA, 0xE420E4, 0xE420E9, 0xE420EC, + 0xE420ED, 0xE420EF, 0xE420F8, 0xE420FA, 0xE4EC20, 0xE5E020, 0xE5E420, 0xE7E020, 0xE9E020, 0xE9E120, 0xE9E420, 0xEC20E4, 0xEC20ED, 0xEC20FA, 0xECF220, 0xECF920, + 0xEDE9E9, 0xEDE9F0, 0xEDE9F8, 0xEE20E4, 0xEE20ED, 0xEE20FA, 0xEEE120, 0xEEE420, 0xF2E420, 0xF920E4, 0xF920ED, 0xF920FA, 0xF9E420, 0xFAE020, 0xFAE420, 0xFAE5E9, +} + +func newRecognizer_8859_8(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-8", + hasC1ByteCharset: "windows-1255", + language: language, + charMap: &charMap_8859_8, + ngram: ngram, + } +} + +func newRecognizer_8859_8_I_he() *recognizerSingleByte { + r := newRecognizer_8859_8("he", &ngrams_8859_8_I_he) + r.charset = "ISO-8859-8-I" + return r +} + +func newRecognizer_8859_8_he() *recognizerSingleByte { + return newRecognizer_8859_8("he", &ngrams_8859_8_he) +} + +var charMap_8859_9 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0xAA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0xB5, 0x20, 0x20, + 0x20, 0x20, 0xBA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x69, 0xFE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x20, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, +} + +var ngrams_8859_9_tr = [64]uint32{ + 0x206261, 0x206269, 0x206275, 0x206461, 0x206465, 0x206765, 0x206861, 0x20696C, 0x206B61, 0x206B6F, 0x206D61, 0x206F6C, 0x207361, 0x207461, 0x207665, 0x207961, + 0x612062, 0x616B20, 0x616C61, 0x616D61, 0x616E20, 0x616EFD, 0x617220, 0x617261, 0x6172FD, 0x6173FD, 0x617961, 0x626972, 0x646120, 0x646520, 0x646920, 0x652062, + 0x65206B, 0x656469, 0x656E20, 0x657220, 0x657269, 0x657369, 0x696C65, 0x696E20, 0x696E69, 0x697220, 0x6C616E, 0x6C6172, 0x6C6520, 0x6C6572, 0x6E2061, 0x6E2062, + 0x6E206B, 0x6E6461, 0x6E6465, 0x6E6520, 0x6E6920, 0x6E696E, 0x6EFD20, 0x72696E, 0x72FD6E, 0x766520, 0x796120, 0x796F72, 0xFD6E20, 0xFD6E64, 0xFD6EFD, 0xFDF0FD, +} + +func newRecognizer_8859_9(language string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "ISO-8859-9", + hasC1ByteCharset: "windows-1254", + language: language, + charMap: &charMap_8859_9, + ngram: ngram, + } +} + +func newRecognizer_8859_9_tr() *recognizerSingleByte { + return newRecognizer_8859_9("tr", &ngrams_8859_9_tr) +} + +var charMap_windows_1256 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x81, 0x20, 0x83, 0x20, 0x20, 0x20, 0x20, + 0x88, 0x20, 0x8A, 0x20, 0x9C, 0x8D, 0x8E, 0x8F, + 0x90, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x98, 0x20, 0x9A, 0x20, 0x9C, 0x20, 0x20, 0x9F, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0xAA, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0xB5, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0x20, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0x20, 0x20, 0x20, 0x20, 0xF4, 0x20, 0x20, 0x20, + 0x20, 0xF9, 0x20, 0xFB, 0xFC, 0x20, 0x20, 0xFF, +} + +var ngrams_windows_1256 = [64]uint32{ + 0x20C7E1, 0x20C7E4, 0x20C8C7, 0x20DAE1, 0x20DDED, 0x20E1E1, 0x20E3E4, 0x20E6C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E120, 0xC7E1C3, 0xC7E1C7, 0xC7E1C8, + 0xC7E1CA, 0xC7E1CC, 0xC7E1CD, 0xC7E1CF, 0xC7E1D3, 0xC7E1DA, 0xC7E1DE, 0xC7E1E3, 0xC7E1E6, 0xC7E1ED, 0xC7E320, 0xC7E420, 0xC7E4CA, 0xC820C7, 0xC920C7, 0xC920DD, + 0xC920E1, 0xC920E3, 0xC920E6, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xDA20C7, 0xDAE1EC, 0xDDED20, 0xE120C7, 0xE1C920, 0xE1EC20, 0xE1ED20, + 0xE320C7, 0xE3C720, 0xE3C920, 0xE3E420, 0xE420C7, 0xE520C7, 0xE5C720, 0xE6C7E1, 0xE6E420, 0xEC20C7, 0xED20C7, 0xED20E3, 0xED20E6, 0xEDC920, 0xEDD120, 0xEDE420, +} + +func newRecognizer_windows_1256() *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "windows-1256", + language: "ar", + charMap: &charMap_windows_1256, + ngram: &ngrams_windows_1256, + } +} + +var charMap_windows_1251 = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x90, 0x83, 0x20, 0x83, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x9A, 0x20, 0x9C, 0x9D, 0x9E, 0x9F, + 0x90, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x9A, 0x20, 0x9C, 0x9D, 0x9E, 0x9F, + 0x20, 0xA2, 0xA2, 0xBC, 0x20, 0xB4, 0x20, 0x20, + 0xB8, 0x20, 0xBA, 0x20, 0x20, 0x20, 0x20, 0xBF, + 0x20, 0x20, 0xB3, 0xB3, 0xB4, 0xB5, 0x20, 0x20, + 0xB8, 0x20, 0xBA, 0x20, 0xBC, 0xBE, 0xBE, 0xBF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, +} + +var ngrams_windows_1251 = [64]uint32{ + 0x20E220, 0x20E2EE, 0x20E4EE, 0x20E7E0, 0x20E820, 0x20EAE0, 0x20EAEE, 0x20EDE0, 0x20EDE5, 0x20EEE1, 0x20EFEE, 0x20EFF0, 0x20F0E0, 0x20F1EE, 0x20F1F2, 0x20F2EE, + 0x20F7F2, 0x20FDF2, 0xE0EDE8, 0xE0F2FC, 0xE3EE20, 0xE5EBFC, 0xE5EDE8, 0xE5F1F2, 0xE5F220, 0xE820EF, 0xE8E520, 0xE8E820, 0xE8FF20, 0xEBE5ED, 0xEBE820, 0xEBFCED, + 0xEDE020, 0xEDE520, 0xEDE8E5, 0xEDE8FF, 0xEDEE20, 0xEDEEE2, 0xEE20E2, 0xEE20EF, 0xEE20F1, 0xEEE220, 0xEEE2E0, 0xEEE3EE, 0xEEE920, 0xEEEBFC, 0xEEEC20, 0xEEF1F2, + 0xEFEEEB, 0xEFF0E5, 0xEFF0E8, 0xEFF0EE, 0xF0E0E2, 0xF0E5E4, 0xF1F2E0, 0xF1F2E2, 0xF1F2E8, 0xF1FF20, 0xF2E5EB, 0xF2EE20, 0xF2EEF0, 0xF2FC20, 0xF7F2EE, 0xFBF520, +} + +func newRecognizer_windows_1251() *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "windows-1251", + language: "ar", + charMap: &charMap_windows_1251, + ngram: &ngrams_windows_1251, + } +} + +var charMap_KOI8_R = [256]byte{ + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0xA3, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0xA3, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, + 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, +} + +var ngrams_KOI8_R = [64]uint32{ + 0x20C4CF, 0x20C920, 0x20CBC1, 0x20CBCF, 0x20CEC1, 0x20CEC5, 0x20CFC2, 0x20D0CF, 0x20D0D2, 0x20D2C1, 0x20D3CF, 0x20D3D4, 0x20D4CF, 0x20D720, 0x20D7CF, 0x20DAC1, + 0x20DCD4, 0x20DED4, 0xC1CEC9, 0xC1D4D8, 0xC5CCD8, 0xC5CEC9, 0xC5D3D4, 0xC5D420, 0xC7CF20, 0xC920D0, 0xC9C520, 0xC9C920, 0xC9D120, 0xCCC5CE, 0xCCC920, 0xCCD8CE, + 0xCEC120, 0xCEC520, 0xCEC9C5, 0xCEC9D1, 0xCECF20, 0xCECFD7, 0xCF20D0, 0xCF20D3, 0xCF20D7, 0xCFC7CF, 0xCFCA20, 0xCFCCD8, 0xCFCD20, 0xCFD3D4, 0xCFD720, 0xCFD7C1, + 0xD0CFCC, 0xD0D2C5, 0xD0D2C9, 0xD0D2CF, 0xD2C1D7, 0xD2C5C4, 0xD3D120, 0xD3D4C1, 0xD3D4C9, 0xD3D4D7, 0xD4C5CC, 0xD4CF20, 0xD4CFD2, 0xD4D820, 0xD9C820, 0xDED4CF, +} + +func newRecognizer_KOI8_R() *recognizerSingleByte { + return &recognizerSingleByte{ + charset: "KOI8-R", + language: "ru", + charMap: &charMap_KOI8_R, + ngram: &ngrams_KOI8_R, + } +} + +var charMap_IBM424_he = [256]byte{ + /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ + /* 0- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 1- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 2- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 3- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 4- */ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 5- */ 0x40, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 6- */ 0x40, 0x40, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 7- */ 0x40, 0x71, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x40, 0x40, + /* 8- */ 0x40, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 9- */ 0x40, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* A- */ 0xA0, 0x40, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* B- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* C- */ 0x40, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* D- */ 0x40, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* E- */ 0x40, 0x40, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* F- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, +} + +var ngrams_IBM424_he_rtl = [64]uint32{ + 0x404146, 0x404148, 0x404151, 0x404171, 0x404251, 0x404256, 0x404541, 0x404546, 0x404551, 0x404556, 0x404562, 0x404569, 0x404571, 0x405441, 0x405445, 0x405641, + 0x406254, 0x406954, 0x417140, 0x454041, 0x454042, 0x454045, 0x454054, 0x454056, 0x454069, 0x454641, 0x464140, 0x465540, 0x465740, 0x466840, 0x467140, 0x514045, + 0x514540, 0x514671, 0x515155, 0x515540, 0x515740, 0x516840, 0x517140, 0x544041, 0x544045, 0x544140, 0x544540, 0x554041, 0x554042, 0x554045, 0x554054, 0x554056, + 0x554069, 0x564540, 0x574045, 0x584540, 0x585140, 0x585155, 0x625440, 0x684045, 0x685155, 0x695440, 0x714041, 0x714042, 0x714045, 0x714054, 0x714056, 0x714069, +} + +var ngrams_IBM424_he_ltr = [64]uint32{ + 0x404146, 0x404154, 0x404551, 0x404554, 0x404556, 0x404558, 0x405158, 0x405462, 0x405469, 0x405546, 0x405551, 0x405746, 0x405751, 0x406846, 0x406851, 0x407141, + 0x407146, 0x407151, 0x414045, 0x414054, 0x414055, 0x414071, 0x414540, 0x414645, 0x415440, 0x415640, 0x424045, 0x424055, 0x424071, 0x454045, 0x454051, 0x454054, + 0x454055, 0x454057, 0x454068, 0x454071, 0x455440, 0x464140, 0x464540, 0x484140, 0x514140, 0x514240, 0x514540, 0x544045, 0x544055, 0x544071, 0x546240, 0x546940, + 0x555151, 0x555158, 0x555168, 0x564045, 0x564055, 0x564071, 0x564240, 0x564540, 0x624540, 0x694045, 0x694055, 0x694071, 0x694540, 0x714140, 0x714540, 0x714651, +} + +func newRecognizer_IBM424_he(charset string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: charset, + language: "he", + charMap: &charMap_IBM424_he, + ngram: ngram, + } +} + +func newRecognizer_IBM424_he_rtl() *recognizerSingleByte { + return newRecognizer_IBM424_he("IBM424_rtl", &ngrams_IBM424_he_rtl) +} + +func newRecognizer_IBM424_he_ltr() *recognizerSingleByte { + return newRecognizer_IBM424_he("IBM424_ltr", &ngrams_IBM424_he_ltr) +} + +var charMap_IBM420_ar = [256]byte{ + /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ + /* 0- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 1- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 2- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 3- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 4- */ 0x40, 0x40, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 5- */ 0x40, 0x51, 0x52, 0x40, 0x40, 0x55, 0x56, 0x57, 0x58, 0x59, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 6- */ 0x40, 0x40, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 7- */ 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + /* 8- */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + /* 9- */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + /* A- */ 0xA0, 0x40, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + /* B- */ 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0x40, 0x40, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, + /* C- */ 0x40, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x40, 0xCB, 0x40, 0xCD, 0x40, 0xCF, + /* D- */ 0x40, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + /* E- */ 0x40, 0x40, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xEA, 0xEB, 0x40, 0xED, 0xEE, 0xEF, + /* F- */ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0xFB, 0xFC, 0xFD, 0xFE, 0x40, +} + +var ngrams_IBM420_ar_rtl = [64]uint32{ + 0x4056B1, 0x4056BD, 0x405856, 0x409AB1, 0x40ABDC, 0x40B1B1, 0x40BBBD, 0x40CF56, 0x564056, 0x564640, 0x566340, 0x567540, 0x56B140, 0x56B149, 0x56B156, 0x56B158, + 0x56B163, 0x56B167, 0x56B169, 0x56B173, 0x56B178, 0x56B19A, 0x56B1AD, 0x56B1BB, 0x56B1CF, 0x56B1DC, 0x56BB40, 0x56BD40, 0x56BD63, 0x584056, 0x624056, 0x6240AB, + 0x6240B1, 0x6240BB, 0x6240CF, 0x634056, 0x734056, 0x736240, 0x754056, 0x756240, 0x784056, 0x9A4056, 0x9AB1DA, 0xABDC40, 0xB14056, 0xB16240, 0xB1DA40, 0xB1DC40, + 0xBB4056, 0xBB5640, 0xBB6240, 0xBBBD40, 0xBD4056, 0xBF4056, 0xBF5640, 0xCF56B1, 0xCFBD40, 0xDA4056, 0xDC4056, 0xDC40BB, 0xDC40CF, 0xDC6240, 0xDC7540, 0xDCBD40, +} + +var ngrams_IBM420_ar_ltr = [64]uint32{ + 0x404656, 0x4056BB, 0x4056BF, 0x406273, 0x406275, 0x4062B1, 0x4062BB, 0x4062DC, 0x406356, 0x407556, 0x4075DC, 0x40B156, 0x40BB56, 0x40BD56, 0x40BDBB, 0x40BDCF, + 0x40BDDC, 0x40DAB1, 0x40DCAB, 0x40DCB1, 0x49B156, 0x564056, 0x564058, 0x564062, 0x564063, 0x564073, 0x564075, 0x564078, 0x56409A, 0x5640B1, 0x5640BB, 0x5640BD, + 0x5640BF, 0x5640DA, 0x5640DC, 0x565840, 0x56B156, 0x56CF40, 0x58B156, 0x63B156, 0x63BD56, 0x67B156, 0x69B156, 0x73B156, 0x78B156, 0x9AB156, 0xAB4062, 0xADB156, + 0xB14062, 0xB15640, 0xB156CF, 0xB19A40, 0xB1B140, 0xBB4062, 0xBB40DC, 0xBBB156, 0xBD5640, 0xBDBB40, 0xCF4062, 0xCF40DC, 0xCFB156, 0xDAB19A, 0xDCAB40, 0xDCB156, +} + +func newRecognizer_IBM420_ar(charset string, ngram *[64]uint32) *recognizerSingleByte { + return &recognizerSingleByte{ + charset: charset, + language: "ar", + charMap: &charMap_IBM420_ar, + ngram: ngram, + } +} + +func newRecognizer_IBM420_ar_rtl() *recognizerSingleByte { + return newRecognizer_IBM420_ar("IBM420_rtl", &ngrams_IBM420_ar_rtl) +} + +func newRecognizer_IBM420_ar_ltr() *recognizerSingleByte { + return newRecognizer_IBM420_ar("IBM420_ltr", &ngrams_IBM420_ar_ltr) +} diff --git a/vendor/github.com/gogs/chardet/unicode.go b/vendor/github.com/gogs/chardet/unicode.go new file mode 100644 index 000000000..6f9fa9e67 --- /dev/null +++ b/vendor/github.com/gogs/chardet/unicode.go @@ -0,0 +1,103 @@ +package chardet + +import ( + "bytes" +) + +var ( + utf16beBom = []byte{0xFE, 0xFF} + utf16leBom = []byte{0xFF, 0xFE} + utf32beBom = []byte{0x00, 0x00, 0xFE, 0xFF} + utf32leBom = []byte{0xFF, 0xFE, 0x00, 0x00} +) + +type recognizerUtf16be struct { +} + +func newRecognizer_utf16be() *recognizerUtf16be { + return &recognizerUtf16be{} +} + +func (*recognizerUtf16be) Match(input *recognizerInput) (output recognizerOutput) { + output = recognizerOutput{ + Charset: "UTF-16BE", + } + if bytes.HasPrefix(input.raw, utf16beBom) { + output.Confidence = 100 + } + return +} + +type recognizerUtf16le struct { +} + +func newRecognizer_utf16le() *recognizerUtf16le { + return &recognizerUtf16le{} +} + +func (*recognizerUtf16le) Match(input *recognizerInput) (output recognizerOutput) { + output = recognizerOutput{ + Charset: "UTF-16LE", + } + if bytes.HasPrefix(input.raw, utf16leBom) && !bytes.HasPrefix(input.raw, utf32leBom) { + output.Confidence = 100 + } + return +} + +type recognizerUtf32 struct { + name string + bom []byte + decodeChar func(input []byte) uint32 +} + +func decodeUtf32be(input []byte) uint32 { + return uint32(input[0])<<24 | uint32(input[1])<<16 | uint32(input[2])<<8 | uint32(input[3]) +} + +func decodeUtf32le(input []byte) uint32 { + return uint32(input[3])<<24 | uint32(input[2])<<16 | uint32(input[1])<<8 | uint32(input[0]) +} + +func newRecognizer_utf32be() *recognizerUtf32 { + return &recognizerUtf32{ + "UTF-32BE", + utf32beBom, + decodeUtf32be, + } +} + +func newRecognizer_utf32le() *recognizerUtf32 { + return &recognizerUtf32{ + "UTF-32LE", + utf32leBom, + decodeUtf32le, + } +} + +func (r *recognizerUtf32) Match(input *recognizerInput) (output recognizerOutput) { + output = recognizerOutput{ + Charset: r.name, + } + hasBom := bytes.HasPrefix(input.raw, r.bom) + var numValid, numInvalid uint32 + for b := input.raw; len(b) >= 4; b = b[4:] { + if c := r.decodeChar(b); c >= 0x10FFFF || (c >= 0xD800 && c <= 0xDFFF) { + numInvalid++ + } else { + numValid++ + } + } + if hasBom && numInvalid == 0 { + output.Confidence = 100 + } else if hasBom && numValid > numInvalid*10 { + output.Confidence = 80 + } else if numValid > 3 && numInvalid == 0 { + output.Confidence = 100 + } else if numValid > 0 && numInvalid == 0 { + output.Confidence = 80 + } else if numValid > numInvalid*10 { + output.Confidence = 25 + } + return +} diff --git a/vendor/github.com/gogs/chardet/utf8.go b/vendor/github.com/gogs/chardet/utf8.go new file mode 100644 index 000000000..ae036ad9b --- /dev/null +++ b/vendor/github.com/gogs/chardet/utf8.go @@ -0,0 +1,71 @@ +package chardet + +import ( + "bytes" +) + +var utf8Bom = []byte{0xEF, 0xBB, 0xBF} + +type recognizerUtf8 struct { +} + +func newRecognizer_utf8() *recognizerUtf8 { + return &recognizerUtf8{} +} + +func (*recognizerUtf8) Match(input *recognizerInput) (output recognizerOutput) { + output = recognizerOutput{ + Charset: "UTF-8", + } + hasBom := bytes.HasPrefix(input.raw, utf8Bom) + inputLen := len(input.raw) + var numValid, numInvalid uint32 + var trailBytes uint8 + for i := 0; i < inputLen; i++ { + c := input.raw[i] + if c&0x80 == 0 { + continue + } + if c&0xE0 == 0xC0 { + trailBytes = 1 + } else if c&0xF0 == 0xE0 { + trailBytes = 2 + } else if c&0xF8 == 0xF0 { + trailBytes = 3 + } else { + numInvalid++ + if numInvalid > 5 { + break + } + trailBytes = 0 + } + + for i++; i < inputLen; i++ { + c = input.raw[i] + if c&0xC0 != 0x80 { + numInvalid++ + break + } + if trailBytes--; trailBytes == 0 { + numValid++ + break + } + } + } + + if hasBom && numInvalid == 0 { + output.Confidence = 100 + } else if hasBom && numValid > numInvalid*10 { + output.Confidence = 80 + } else if numValid > 3 && numInvalid == 0 { + output.Confidence = 100 + } else if numValid > 0 && numInvalid == 0 { + output.Confidence = 80 + } else if numValid == 0 && numInvalid == 0 { + // Plain ASCII + output.Confidence = 10 + } else if numValid > numInvalid*10 { + output.Confidence = 25 + } + return +} diff --git a/vendor/github.com/inbucket/html2text/.gitignore b/vendor/github.com/inbucket/html2text/.gitignore new file mode 100644 index 000000000..da3741f53 --- /dev/null +++ b/vendor/github.com/inbucket/html2text/.gitignore @@ -0,0 +1,26 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +.direnv +.envrc diff --git a/vendor/github.com/inbucket/html2text/LICENSE b/vendor/github.com/inbucket/html2text/LICENSE new file mode 100644 index 000000000..24dc4abec --- /dev/null +++ b/vendor/github.com/inbucket/html2text/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jay Taylor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/inbucket/html2text/README.md b/vendor/github.com/inbucket/html2text/README.md new file mode 100644 index 000000000..263c7dd48 --- /dev/null +++ b/vendor/github.com/inbucket/html2text/README.md @@ -0,0 +1,132 @@ +# html2text + +[![Go Reference](https://pkg.go.dev/badge/github.com/inbucket/html2text.svg)](https://pkg.go.dev/github.com/inbucket/html2text) +[![Build and Test](https://github.com/inbucket/html2text/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/inbucket/html2text/actions/workflows/build-and-test.yml) +[![Report Card](https://goreportcard.com/badge/github.com/inbucket/html2text)](https://goreportcard.com/report/github.com/inbucket/html2text) + +### Converts HTML into text of the markdown-flavored variety + +This is a permanent fork of the original +[jaytaylor.com/html2text](https://github.com/jaytaylor/html2text) package. + +## Introduction + +Ensure your emails are readable by all! + +Turns HTML into raw text, useful for sending fancy HTML emails with an equivalently nicely formatted TXT document as a fallback (e.g. for people who don't allow HTML emails or have other display issues). + +html2text is a simple golang package for rendering HTML into plaintext. + +There are still lots of improvements to be had, but FWIW this has worked fine for my [basic] HTML-2-text needs. + +It requires go 1.x or newer ;) + +## Download the package + +```bash +go get github.com/inbucket/html2text +``` + +## Example usage + +### Library + +```go +package main + +import ( + "fmt" + + "github.com/inbucket/html2text" +) + +func main() { + inputHTML := ` + + + My Mega Service + + + + + + + +

Welcome to your new account on my service!

+ +

+ Here is some more information: + +

+

+ + + + + + + + + + + + +
Header 1Header 2
Footer 1Footer 2
Row 1 Col 1Row 1 Col 2
Row 2 Col 1Row 2 Col 2
+ +` + + text, err := html2text.FromString(inputHTML, html2text.Options{PrettyTables: true}) + if err != nil { + panic(err) + } + fmt.Println(text) +} +``` + +Output: +``` +Mega Service ( http://jaytaylor.com/ ) + +****************************************** +Welcome to your new account on my service! +****************************************** + +Here is some more information: + +* Link 1: Example.com ( https://example.com ) +* Link 2: Example2.com ( https://example2.com ) +* Something else + ++-------------+-------------+ +| HEADER 1 | HEADER 2 | ++-------------+-------------+ +| Row 1 Col 1 | Row 1 Col 2 | +| Row 2 Col 1 | Row 2 Col 2 | ++-------------+-------------+ +| FOOTER 1 | FOOTER 2 | ++-------------+-------------+ +``` + +### Command line + +``` +echo '
hi
' | html2text +``` + +## Unit-tests + +Running the unit-tests is straightforward and standard: + +```bash +go test +``` + +# License + +Permissive MIT license. diff --git a/vendor/github.com/inbucket/html2text/flake.lock b/vendor/github.com/inbucket/html2text/flake.lock new file mode 100644 index 000000000..5ca5bd310 --- /dev/null +++ b/vendor/github.com/inbucket/html2text/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1749369410, + "narHash": "sha256-P1eYm8bewiyWg3QaPCHrOp6iWg/7ESi/aGHT4yilyNo=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "8207ad0d501dd3590e0e81a7c56b386a5c4342c9", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/vendor/github.com/inbucket/html2text/flake.nix b/vendor/github.com/inbucket/html2text/flake.nix new file mode 100644 index 000000000..4c5c3a05a --- /dev/null +++ b/vendor/github.com/inbucket/html2text/flake.nix @@ -0,0 +1,17 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShell = pkgs.callPackage ./shell.nix { }; + } + ); +} diff --git a/vendor/github.com/inbucket/html2text/html2text.go b/vendor/github.com/inbucket/html2text/html2text.go new file mode 100644 index 000000000..05aeeb328 --- /dev/null +++ b/vendor/github.com/inbucket/html2text/html2text.go @@ -0,0 +1,498 @@ +package html2text + +import ( + "bytes" + "io" + "regexp" + "strings" + "unicode" + + "github.com/olekukonko/tablewriter" + "github.com/ssor/bom" + "golang.org/x/net/html" + "golang.org/x/net/html/atom" +) + +// Options provide toggles and overrides to control specific rendering behaviors. +type Options struct { + PrettyTables bool // Turns on pretty ASCII rendering for table elements. + PrettyTablesOptions *PrettyTablesOptions // Configures pretty ASCII rendering for table elements. + OmitLinks bool // Turns on omitting links + TextOnly bool // Returns only plain text +} + +// FromHTMLNode renders text output from a pre-parsed HTML document. +func FromHTMLNode(doc *html.Node, o ...Options) (string, error) { + var options Options + if len(o) > 0 { + options = o[0] + } + + if options.PrettyTables && options.PrettyTablesOptions == nil { + // defaults need to make explicit as they are no longer identical with tablewriter + options.PrettyTablesOptions = NewPrettyTablesOptions() + } + + ctx := textifyTraverseContext{ + buf: bytes.Buffer{}, + options: options, + } + if err := ctx.traverse(doc); err != nil { + return "", err + } + + text := strings.TrimSpace(newlineRe.ReplaceAllString( + strings.Replace(ctx.buf.String(), "\n ", "\n", -1), "\n\n"), + ) + return text, nil +} + +// FromReader renders text output after parsing HTML for the specified +// io.Reader. +func FromReader(reader io.Reader, options ...Options) (string, error) { + newReader, err := bom.NewReaderWithoutBom(reader) + if err != nil { + return "", err + } + doc, err := html.Parse(newReader) + if err != nil { + return "", err + } + return FromHTMLNode(doc, options...) +} + +// FromString parses HTML from the input string, then renders the text form. +func FromString(input string, options ...Options) (string, error) { + bs := bom.CleanBom([]byte(input)) + text, err := FromReader(bytes.NewReader(bs), options...) + if err != nil { + return "", err + } + return text, nil +} + +var ( + spacingRe = regexp.MustCompile(`[ \r\n\t]+`) + newlineRe = regexp.MustCompile(`\n\n+`) +) + +// traverseTableCtx holds text-related context. +type textifyTraverseContext struct { + buf bytes.Buffer + + prefix string + tableCtx tableTraverseContext + options Options + endsWithSpace bool + justClosedDiv bool + blockquoteLevel int + lineLength int + isPre bool +} + +// tableTraverseContext holds table ASCII-form related context. +type tableTraverseContext struct { + header []string + body [][]string + footer []string + tmpRow int + isInFooter bool +} + +func (tableCtx *tableTraverseContext) init() { + tableCtx.body = [][]string{} + tableCtx.header = []string{} + tableCtx.footer = []string{} + tableCtx.isInFooter = false + tableCtx.tmpRow = 0 +} + +func (ctx *textifyTraverseContext) handleElement(node *html.Node) error { + ctx.justClosedDiv = false + + switch node.DataAtom { + case atom.Br: + return ctx.emit("\n") + + case atom.H1, atom.H2, atom.H3: + subCtx := textifyTraverseContext{} + if err := subCtx.traverseChildren(node); err != nil { + return err + } + + str := subCtx.buf.String() + if ctx.options.TextOnly { + return ctx.emit(str + ".\n\n") + } + dividerLen := 0 + for _, line := range strings.Split(str, "\n") { + if lineLen := len([]rune(line)); lineLen-1 > dividerLen { + dividerLen = lineLen - 1 + } + } + var divider string + if node.DataAtom == atom.H1 { + divider = strings.Repeat("*", dividerLen) + } else { + divider = strings.Repeat("-", dividerLen) + } + + if node.DataAtom == atom.H3 { + return ctx.emit("\n\n" + str + "\n" + divider + "\n\n") + } + return ctx.emit("\n\n" + divider + "\n" + str + "\n" + divider + "\n\n") + + case atom.Blockquote: + ctx.blockquoteLevel++ + if !ctx.options.TextOnly { + ctx.prefix = strings.Repeat(">", ctx.blockquoteLevel) + " " + } + if err := ctx.emit("\n"); err != nil { + return err + } + if ctx.blockquoteLevel == 1 { + if err := ctx.emit("\n"); err != nil { + return err + } + } + if err := ctx.traverseChildren(node); err != nil { + return err + } + ctx.blockquoteLevel-- + if !ctx.options.TextOnly { + ctx.prefix = strings.Repeat(">", ctx.blockquoteLevel) + } + if ctx.blockquoteLevel > 0 { + ctx.prefix += " " + } + return ctx.emit("\n\n") + + case atom.Div: + if ctx.lineLength > 0 { + if err := ctx.emit("\n"); err != nil { + return err + } + } + if err := ctx.traverseChildren(node); err != nil { + return err + } + var err error + if !ctx.justClosedDiv { + err = ctx.emit("\n") + } + ctx.justClosedDiv = true + return err + + case atom.Li: + if !ctx.options.TextOnly { + if err := ctx.emit("* "); err != nil { + return err + } + } + + if err := ctx.traverseChildren(node); err != nil { + return err + } + + return ctx.emit("\n") + + case atom.B, atom.Strong: + subCtx := textifyTraverseContext{} + subCtx.endsWithSpace = true + if err := subCtx.traverseChildren(node); err != nil { + return err + } + str := subCtx.buf.String() + if ctx.options.TextOnly { + return ctx.emit(str + ".") + } + return ctx.emit("*" + str + "*") + + case atom.A: + linkText := "" + // For simple link element content with single text node only, peek at the link text. + if node.FirstChild != nil && node.FirstChild.NextSibling == nil && node.FirstChild.Type == html.TextNode { + linkText = node.FirstChild.Data + } + + // If image is the only child, take its alt text as the link text. + if img := node.FirstChild; img != nil && node.LastChild == img && img.DataAtom == atom.Img { + if altText := getAttrVal(img, "alt"); altText != "" { + if err := ctx.emit(altText); err != nil { + return err + } + } + } else if err := ctx.traverseChildren(node); err != nil { + return err + } + + hrefLink := "" + if attrVal := getAttrVal(node, "href"); attrVal != "" { + attrVal = ctx.normalizeHrefLink(attrVal) + // Don't print link href if it matches link element content or if the link is empty. + if (attrVal != "" && linkText != attrVal) && !ctx.options.OmitLinks && !ctx.options.TextOnly { + hrefLink = "( " + attrVal + " )" + } + } + + return ctx.emit(hrefLink) + + case atom.P, atom.Ul: + return ctx.paragraphHandler(node) + + case atom.Table, atom.Tfoot, atom.Th, atom.Tr, atom.Td: + if ctx.options.PrettyTables { + return ctx.handleTableElement(node) + } else if node.DataAtom == atom.Table { + return ctx.paragraphHandler(node) + } + return ctx.traverseChildren(node) + + case atom.Pre: + ctx.isPre = true + err := ctx.traverseChildren(node) + ctx.isPre = false + return err + + case atom.Style, atom.Script, atom.Head: + // Ignore the subtree. + return nil + + default: + return ctx.traverseChildren(node) + } +} + +// paragraphHandler renders node children surrounded by double newlines. +func (ctx *textifyTraverseContext) paragraphHandler(node *html.Node) error { + if err := ctx.emit("\n\n"); err != nil { + return err + } + if err := ctx.traverseChildren(node); err != nil { + return err + } + return ctx.emit("\n\n") +} + +// handleTableElement is only to be invoked when options.PrettyTables is active. +func (ctx *textifyTraverseContext) handleTableElement(node *html.Node) error { + if !ctx.options.PrettyTables { + panic("handleTableElement invoked when PrettyTables not active") + } + + switch node.DataAtom { + case atom.Table: + if err := ctx.emit("\n\n"); err != nil { + return err + } + + // Re-intialize all table context. + ctx.tableCtx.init() + + // Browse children, enriching context with table data. + if err := ctx.traverseChildren(node); err != nil { + return err + } + + buf := &bytes.Buffer{} + table := tablewriter.NewWriter(buf) + ctx.options.PrettyTablesOptions.configureTable(table) + + table.Header(ctx.tableCtx.header) + table.Footer(ctx.tableCtx.footer) + if err := table.Bulk(ctx.tableCtx.body); err != nil { + return err + } + + // Render the table using ASCII. + if err := table.Render(); err != nil { + return err + } + if err := ctx.emit(buf.String()); err != nil { + return err + } + + return ctx.emit("\n\n") + + case atom.Tfoot: + ctx.tableCtx.isInFooter = true + if err := ctx.traverseChildren(node); err != nil { + return err + } + ctx.tableCtx.isInFooter = false + + case atom.Tr: + ctx.tableCtx.body = append(ctx.tableCtx.body, []string{}) + if err := ctx.traverseChildren(node); err != nil { + return err + } + ctx.tableCtx.tmpRow++ + + case atom.Th: + res, err := ctx.renderEachChild(node) + if err != nil { + return err + } + + ctx.tableCtx.header = append(ctx.tableCtx.header, res) + + case atom.Td: + res, err := ctx.renderEachChild(node) + if err != nil { + return err + } + + if ctx.tableCtx.isInFooter { + ctx.tableCtx.footer = append(ctx.tableCtx.footer, res) + } else { + ctx.tableCtx.body[ctx.tableCtx.tmpRow] = append(ctx.tableCtx.body[ctx.tableCtx.tmpRow], res) + } + + } + return nil +} + +func (ctx *textifyTraverseContext) traverse(node *html.Node) error { + switch node.Type { + default: + return ctx.traverseChildren(node) + + case html.TextNode: + var data string + if ctx.isPre { + data = node.Data + } else { + data = strings.TrimSpace(spacingRe.ReplaceAllString(node.Data, " ")) + } + return ctx.emit(data) + + case html.ElementNode: + return ctx.handleElement(node) + } +} + +func (ctx *textifyTraverseContext) traverseChildren(node *html.Node) error { + for c := node.FirstChild; c != nil; c = c.NextSibling { + if err := ctx.traverse(c); err != nil { + return err + } + } + + return nil +} + +func (ctx *textifyTraverseContext) emit(data string) error { + if data == "" { + return nil + } + var ( + lines = ctx.breakLongLines(data) + err error + ) + for _, line := range lines { + runes := []rune(line) + startsWithSpace := unicode.IsSpace(runes[0]) + if !startsWithSpace && !ctx.endsWithSpace && !strings.HasPrefix(data, ".") { + if err = ctx.buf.WriteByte(' '); err != nil { + return err + } + ctx.lineLength++ + } + ctx.endsWithSpace = unicode.IsSpace(runes[len(runes)-1]) + for _, c := range line { + if _, err = ctx.buf.WriteString(string(c)); err != nil { + return err + } + ctx.lineLength++ + if c == '\n' { + ctx.lineLength = 0 + if ctx.prefix != "" { + if _, err = ctx.buf.WriteString(ctx.prefix); err != nil { + return err + } + } + } + } + } + return nil +} + +const maxLineLen = 74 + +func (ctx *textifyTraverseContext) breakLongLines(data string) []string { + // Only break lines when in blockquotes. + if ctx.blockquoteLevel == 0 { + return []string{data} + } + var ( + ret = []string{} + runes = []rune(data) + l = len(runes) + existing = ctx.lineLength + ) + if existing >= maxLineLen { + ret = append(ret, "\n") + existing = 0 + } + for l+existing > maxLineLen { + i := maxLineLen - existing + for i >= 0 && !unicode.IsSpace(runes[i]) { + i-- + } + if i == -1 { + // No spaces, so go the other way. + i = maxLineLen - existing + for i < l && !unicode.IsSpace(runes[i]) { + i++ + } + } + ret = append(ret, string(runes[:i])+"\n") + for i < l && unicode.IsSpace(runes[i]) { + i++ + } + runes = runes[i:] + l = len(runes) + existing = 0 + } + if len(runes) > 0 { + ret = append(ret, string(runes)) + } + return ret +} + +func (ctx *textifyTraverseContext) normalizeHrefLink(link string) string { + link = strings.TrimSpace(link) + link = strings.TrimPrefix(link, "mailto:") + return link +} + +// renderEachChild visits each direct child of a node and collects the sequence of +// textuual representaitons separated by a single newline. +func (ctx *textifyTraverseContext) renderEachChild(node *html.Node) (string, error) { + buf := &bytes.Buffer{} + for c := node.FirstChild; c != nil; c = c.NextSibling { + s, err := FromHTMLNode(c, ctx.options) + if err != nil { + return "", err + } + if _, err = buf.WriteString(s); err != nil { + return "", err + } + if c.NextSibling != nil { + if err = buf.WriteByte('\n'); err != nil { + return "", err + } + } + } + return buf.String(), nil +} + +func getAttrVal(node *html.Node, attrName string) string { + for _, attr := range node.Attr { + if attr.Key == attrName { + return attr.Val + } + } + + return "" +} diff --git a/vendor/github.com/inbucket/html2text/prettytables.go b/vendor/github.com/inbucket/html2text/prettytables.go new file mode 100644 index 000000000..273483d58 --- /dev/null +++ b/vendor/github.com/inbucket/html2text/prettytables.go @@ -0,0 +1,212 @@ +package html2text + +import ( + "github.com/olekukonko/tablewriter" + "github.com/olekukonko/tablewriter/tw" +) + +// PrettyTablesOptions overrides tablewriter behaviors +type PrettyTablesOptions struct { + AutoFormatHeader bool + AutoWrapText bool + // Deprecated. Tablewriter always assumes this to be `true` + ReflowDuringAutoWrap bool + ColWidth int + ColumnSeparator string + RowSeparator string + CenterSeparator string + HeaderAlignment tw.Align + FooterAlignment tw.Align + Alignment tw.Align + ColumnAlignment tw.Alignment + // Deprecated. Tablewriter always assumes this to be `\n` + NewLine string + HeaderLine bool + RowLine bool + AutoMergeCells bool + Borders Border + // Configuration allows to directly manipulate the `Table` with all what [tablewriter] offers. + // Setting this ignores all the rest of the settings of this struct. + Configuration func(table *tablewriter.Table) +} + +// NewPrettyTablesOptions creates PrettyTablesOptions with default settings +func NewPrettyTablesOptions() *PrettyTablesOptions { + return &PrettyTablesOptions{ + AutoFormatHeader: true, + AutoWrapText: true, + ColWidth: 32, // old tablewriter.MAX_ROW_WIDTH + borders now count into width + ColumnSeparator: defaultBorderStyle.ColumnSeparator, + RowSeparator: defaultBorderStyle.RowSeparator, + CenterSeparator: defaultBorderStyle.CenterSeparator, + HeaderAlignment: tw.AlignCenter, + FooterAlignment: tw.AlignCenter, + Alignment: tw.AlignDefault, + ColumnAlignment: make(tw.Alignment, 0), + HeaderLine: true, + RowLine: false, + AutoMergeCells: false, + Borders: Border{Left: true, Right: true, Bottom: true, Top: true}, + } +} + +func (p *PrettyTablesOptions) configureTable(table *tablewriter.Table) { + if p.Configuration != nil { + p.Configuration(table) + return + } + + cfg := tablewriter.NewConfigBuilder() + + cfg.WithHeaderAutoFormat(asState(p.AutoFormatHeader)).WithFooterAutoFormat(asState(p.AutoFormatHeader)). + WithRowAutoWrap(p.wrapMode()).WithHeaderAutoWrap(p.wrapMode()).WithFooterAutoWrap(p.wrapMode()). + WithRowMaxWidth(p.ColWidth). + WithHeaderAlignment(p.HeaderAlignment). + WithFooterAlignment(p.FooterAlignment). + WithRowAlignment(p.Alignment). + WithRowMergeMode(p.mergeMode()) + + if len(p.ColumnAlignment) > 0 { + cfg.Row().Alignment().WithPerColumn(p.ColumnAlignment) + } + + rendition := tw.Rendition{ + Borders: p.Borders.withStates(), + Symbols: p.borderStyle(), + Settings: p.renderSettings(), + } + + table.Options( + tablewriter.WithConfig(cfg.Build()), + tablewriter.WithRendition(rendition)) +} + +func (p *PrettyTablesOptions) wrapMode() int { + if p.AutoWrapText { + return tw.WrapNormal + } else { + return tw.WrapNone + } +} + +func (p *PrettyTablesOptions) mergeMode() int { + if p.AutoMergeCells { + return tw.MergeVertical + } else { + return tw.MergeNone + } +} + +func (p *PrettyTablesOptions) renderSettings() tw.Settings { + return tw.Settings{ + Lines: tw.Lines{ + ShowHeaderLine: asState(p.HeaderLine), + }, + Separators: tw.Separators{ + BetweenRows: asState(p.RowLine), + }, + } +} + +// Border controls tablewriter borders. It uses simple bools instead of tablewriters `State` +type Border struct { + Left, Right, Bottom, Top bool +} + +func (b Border) withStates() tw.Border { + return tw.Border{ + Left: asState(b.Left), + Right: asState(b.Right), + Bottom: asState(b.Bottom), + Top: asState(b.Top), + } +} + +type BorderStyle struct { + ColumnSeparator string + RowSeparator string + CenterSeparator string +} + +func (b BorderStyle) Name() string { + return "html2text" +} + +func (b BorderStyle) Center() string { + return b.CenterSeparator +} + +func (b BorderStyle) Row() string { + return b.RowSeparator +} + +func (b BorderStyle) Column() string { + return b.ColumnSeparator +} + +func (b BorderStyle) TopLeft() string { + return b.CenterSeparator +} + +func (b BorderStyle) TopMid() string { + return b.CenterSeparator +} + +func (b BorderStyle) TopRight() string { + return b.CenterSeparator +} + +func (b BorderStyle) MidLeft() string { + return b.CenterSeparator +} + +func (b BorderStyle) MidRight() string { + return b.CenterSeparator +} + +func (b BorderStyle) BottomLeft() string { + return b.CenterSeparator +} + +func (b BorderStyle) BottomMid() string { + return b.CenterSeparator +} + +func (b BorderStyle) BottomRight() string { + return b.CenterSeparator +} + +func (b BorderStyle) HeaderLeft() string { + return b.CenterSeparator +} + +func (b BorderStyle) HeaderMid() string { + return b.CenterSeparator +} + +func (b BorderStyle) HeaderRight() string { + return b.CenterSeparator +} + +var defaultBorderStyle = BorderStyle{ + ColumnSeparator: "|", + RowSeparator: "-", + CenterSeparator: "+", +} + +func (p *PrettyTablesOptions) borderStyle() BorderStyle { + return BorderStyle{ + ColumnSeparator: p.ColumnSeparator, + RowSeparator: p.RowSeparator, + CenterSeparator: p.CenterSeparator, + } +} + +func asState(b bool) tw.State { + // TableWriter does not provide this by default :( + if b { + return tw.On + } else { + return tw.Off + } +} diff --git a/vendor/github.com/inbucket/html2text/shell.nix b/vendor/github.com/inbucket/html2text/shell.nix new file mode 100644 index 000000000..11538219e --- /dev/null +++ b/vendor/github.com/inbucket/html2text/shell.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } }: +pkgs.mkShell { + buildInputs = with pkgs; [ + delve + go + golangci-lint + gopls + ]; + + hardeningDisable = [ "fortify" ]; +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/.gitattributes b/vendor/github.com/jhillyerd/enmime/v2/.gitattributes new file mode 100644 index 000000000..948bbf8b1 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/.gitattributes @@ -0,0 +1,24 @@ +# Auto detect text files and perform LF normalization +* text=auto +*.golden -text +*.raw -text + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/vendor/github.com/jhillyerd/enmime/v2/.gitignore b/vendor/github.com/jhillyerd/enmime/v2/.gitignore new file mode 100644 index 000000000..31eefb826 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/.gitignore @@ -0,0 +1,33 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +# goland ide +.idea + +# vim swp files +*.swp + +/.direnv + +cmd/mime-dump/mime-dump +cmd/mime-extractor/mime-extractor diff --git a/vendor/github.com/jhillyerd/enmime/v2/.golangci.yml b/vendor/github.com/jhillyerd/enmime/v2/.golangci.yml new file mode 100644 index 000000000..35623e701 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/.golangci.yml @@ -0,0 +1,83 @@ +version: "2" +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - copyloopvar + - decorder + - durationcheck + - errchkjson + - errname + - ginkgolinter + - gocheckcompilerdirectives + - gochecksumtype + - gocritic + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosmopolitan + - grouper + - importas + - inamedparam + - interfacebloat + - loggercheck + - makezero + - mirror + - misspell + - musttag + - nilerr + - noctx + - nolintlint + - nosprintfhostport + - perfsprint + - prealloc + - predeclared + - promlinter + - protogetter + - reassign + - revive + - rowserrcheck + - sloglint + - staticcheck + - tagalign + - tagliatelle + - testableexamples + - testifylint + - thelper + - tparallel + - unparam + - usestdlibvars + - wastedassign + - whitespace + - zerologlint + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + rules: + # Exclude specific linters from test files. + - path: _test\.go + linters: + - gosmopolitan +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/vendor/github.com/jhillyerd/enmime/v2/CONTRIBUTING.md b/vendor/github.com/jhillyerd/enmime/v2/CONTRIBUTING.md new file mode 100644 index 000000000..8cac53505 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/CONTRIBUTING.md @@ -0,0 +1,37 @@ +How to Contribute +================= + +Enmime highly encourages third-party patches. There is a great deal of MIME +encoded email out there, so it's likely you will encounter a scenario we +haven't. + +### tl;dr: + +- Please add a unit test for your fix or feature +- Ensure clean run of `make test lint` + + +## Getting Started + +If you anticipate your issue requiring a large patch, please first submit a +GitHub issue describing the problem or feature. Attach an email that illustrates +the scenario you are trying to improve if possible. You are also encouraged to +outline the process you would like to use to resolve the issue. I will attempt +to provide validation and/or guidance on your suggested approach. + + +## Making Changes + +Create a topic branch based on our `main` branch. + +1. Make commits of logical units. +2. Add unit tests to exercise your changes. +3. **Scrub personally identifying information** from test case emails, and + keep attachments short. +4. Ensure the code builds and tests with `make test` +5. Run the updated code through `make lint` + + +## Thanks + +Thank you for contributing to enmime! diff --git a/vendor/github.com/jhillyerd/enmime/v2/LICENSE b/vendor/github.com/jhillyerd/enmime/v2/LICENSE new file mode 100644 index 000000000..085224f85 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2012-2016 James Hillyerd, All Rights Reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/jhillyerd/enmime/v2/Makefile b/vendor/github.com/jhillyerd/enmime/v2/Makefile new file mode 100644 index 000000000..22cef3bd1 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/Makefile @@ -0,0 +1,33 @@ +SHELL := /bin/sh + +SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*") +PKGS := $(shell go list ./... | grep -v /vendor/) + +.PHONY: all build clean fmt lint reflex simplify test + +all: clean test lint build + +clean: + go clean $(PKGS) + +deps: + go get ./... + +build: + go build + +test: + go test -race ./... + +fmt: + @gofmt -l -w $(SRC) + +simplify: + @gofmt -s -l -w $(SRC) + +lint: + @test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'" + @go vet $(PKGS) + +reflex: + reflex -r '\.go$$' -- sh -c 'echo; date; echo; go test ./... && echo ALL PASS' diff --git a/vendor/github.com/jhillyerd/enmime/v2/README.md b/vendor/github.com/jhillyerd/enmime/v2/README.md new file mode 100644 index 000000000..8755cac9d --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/README.md @@ -0,0 +1,40 @@ +# enmime +[![PkgGoDev](https://pkg.go.dev/badge/github.com/jhillyerd/enmime)][Pkg Docs] +[![Build and Test](https://github.com/jhillyerd/enmime/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/jhillyerd/enmime/actions/workflows/build-and-test.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/jhillyerd/enmime)][Go Report Card] +[![Coverage Status](https://coveralls.io/repos/github/jhillyerd/enmime/badge.svg?branch=main)][Coverage Status] + + +enmime is a MIME encoding and decoding library for Go, focused on generating and +parsing MIME encoded emails. It is being developed in tandem with the +[Inbucket] email service. + +enmime includes a fluent interface builder for generating MIME encoded messages, +see the wiki for example [Builder Usage]. + +See our [Pkg Docs] for examples and API usage information. + + +## Development Status + +enmime is production quality, but there are many buggy MIME encoders in the +wild, so you may still encounter messages it cannot parse. + +See [CONTRIBUTING.md] for more information. + + +## About + +enmime is written in [Go][Golang]. + +enmime is open source software released under the MIT License. The latest +version can be found at https://github.com/jhillyerd/enmime + + +[Builder Usage]: https://github.com/jhillyerd/enmime/wiki/Builder-Usage +[Coverage Status]: https://coveralls.io/github/jhillyerd/enmime +[CONTRIBUTING.md]: https://github.com/jhillyerd/enmime/blob/main/CONTRIBUTING.md +[Inbucket]: https://www.inbucket.org/ +[Golang]: https://go.dev/ +[Go Report Card]: https://goreportcard.com/report/github.com/jhillyerd/enmime +[Pkg Docs]: https://pkg.go.dev/github.com/jhillyerd/enmime diff --git a/vendor/github.com/jhillyerd/enmime/v2/boundary.go b/vendor/github.com/jhillyerd/enmime/v2/boundary.go new file mode 100644 index 000000000..b188b77b3 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/boundary.go @@ -0,0 +1,252 @@ +package enmime + +import ( + "bufio" + "bytes" + stderrors "errors" + "io" + "unicode" + + "github.com/pkg/errors" +) + +// This constant needs to be at least 76 for this package to work correctly. This is because +// \r\n--separator_of_len_70- would fill the buffer and it wouldn't be safe to consume a single byte +// from it. +const peekBufferSize = 4096 + +var errNoBoundaryTerminator = stderrors.New("expected boundary not present") + +type boundaryReader struct { + finished bool // No parts remain when finished + partsRead int // Number of parts read thus far + atPartStart bool // Whether the current part is at its beginning + r *bufio.Reader // Source reader + nlPrefix []byte // NL + MIME boundary prefix + prefix []byte // MIME boundary prefix + final []byte // Final boundary prefix + buffer *bytes.Buffer // Content waiting to be read + unbounded bool // Flag to throw errNoBoundaryTerminator +} + +// newBoundaryReader returns an initialized boundaryReader +func newBoundaryReader(reader *bufio.Reader, boundary string) *boundaryReader { + fullBoundary := []byte("\n--" + boundary + "--") + return &boundaryReader{ + r: reader, + nlPrefix: fullBoundary[:len(fullBoundary)-2], + prefix: fullBoundary[1 : len(fullBoundary)-2], + final: fullBoundary[1:], + buffer: new(bytes.Buffer), + } +} + +// Read returns a buffer containing the content up until boundary +// +// Excerpt from io package on io.Reader implementations: +// +// type Reader interface { +// Read(p []byte) (n int, err error) +// } +// +// Read reads up to len(p) bytes into p. It returns the number of +// bytes read (0 <= n <= len(p)) and any error encountered. Even +// if Read returns n < len(p), it may use all of p as scratch space +// during the call. If some data is available but not len(p) bytes, +// Read conventionally returns what is available instead of waiting +// for more. +// +// When Read encounters an error or end-of-file condition after +// successfully reading n > 0 bytes, it returns the number of bytes +// read. It may return the (non-nil) error from the same call or +// return the error (and n == 0) from a subsequent call. An instance +// of this general case is that a Reader returning a non-zero number +// of bytes at the end of the input stream may return either err == EOF +// or err == nil. The next Read should return 0, EOF. +// +// Callers should always process the n > 0 bytes returned before +// considering the error err. Doing so correctly handles I/O errors +// that happen after reading some bytes and also both of the allowed +// EOF behaviors. +func (b *boundaryReader) Read(dest []byte) (n int, err error) { + if b.buffer.Len() >= len(dest) { + // This read request can be satisfied entirely by the buffer. + n, err = b.buffer.Read(dest) + if b.atPartStart && n > 0 { + b.atPartStart = false + } + + return n, err + } + + for i := 0; i < len(dest); i++ { + var cs []byte + cs, err = b.r.Peek(1) + if err != nil && err != io.EOF { + return 0, errors.WithStack(err) + } + // Ensure that we can switch on the first byte of 'cs' without panic. + if len(cs) > 0 { + padding := 1 + check := false + + switch cs[0] { + // Check for carriage return as potential CRLF boundary prefix. + case '\r': + padding = 2 + check = true + // Check for line feed as potential LF boundary prefix. + case '\n': + check = true + + default: + if b.atPartStart { + // If we're at the very beginning of the part (even before the headers), + // check to see if there's a delimiter that immediately follows. + padding = 0 + check = true + } + } + + if check { + var peek []byte + peek, err = b.r.Peek(len(b.nlPrefix) + padding + 1) + switch err { + case nil: + // Check the whitespace at the head of the peek to avoid checking for a boundary early. + if bytes.HasPrefix(peek, []byte("\n\n")) || + bytes.HasPrefix(peek, []byte("\n\r")) || + bytes.HasPrefix(peek, []byte("\r\n\r")) || + bytes.HasPrefix(peek, []byte("\r\n\n")) { + break + } + // Check the peek buffer for a boundary delimiter or terminator. + if b.isDelimiter(peek[padding:]) || b.isTerminator(peek[padding:]) { + // We have found our boundary terminator, lets write out the final bytes + // and return io.EOF to indicate that this section read is complete. + n, err = b.buffer.Read(dest) + switch err { + case nil, io.EOF: + if b.atPartStart && n > 0 { + b.atPartStart = false + } + return n, io.EOF + default: + return 0, errors.WithStack(err) + } + } + case io.EOF: + // We have reached the end without finding a boundary, + // so we flag the boundary reader to add an error to + // the errors slice and write what we have to the buffer. + b.unbounded = true + default: + continue + } + } + } + + var next byte + next, err = b.r.ReadByte() + if err != nil { + // EOF is not fatal, it just means that we have drained the reader. + if errors.Is(err, io.EOF) { + break + } + + return 0, errors.WithStack(err) + } + + if err = b.buffer.WriteByte(next); err != nil { + return 0, errors.WithStack(err) + } + } + + // Read the contents of the buffer into the destination slice. + n, err = b.buffer.Read(dest) + if b.atPartStart && n > 0 { + b.atPartStart = false + } + return n, err +} + +// Next moves over the boundary to the next part, returns true if there is another part to be read. +func (b *boundaryReader) Next() (bool, error) { + if b.finished { + return false, nil + } + if b.partsRead > 0 { + // Exhaust the current part to prevent errors when moving to the next part. + _, _ = io.Copy(io.Discard, b) + } + for { + var line []byte + var err error + for { + // Read whole line, handle extra long lines in cycle + var segment []byte + segment, err = b.r.ReadSlice('\n') + if line == nil { + line = segment + } else { + line = append(line, segment...) + } + + if err == nil || err == io.EOF { + break + } else if err != bufio.ErrBufferFull || len(segment) == 0 { + return false, errors.WithStack(err) + } + } + + if len(line) > 0 && (line[0] == '\r' || line[0] == '\n') { + // Blank line + continue + } + if b.isTerminator(line) { + b.finished = true + return false, nil + } + if err != io.EOF && b.isDelimiter(line) { + // Start of a new part. + b.partsRead++ + b.atPartStart = true + return true, nil + } + if err == io.EOF { + // Intentionally not wrapping with stack. + return false, io.EOF + } + if b.partsRead == 0 { + // The first part didn't find the starting delimiter, burn off any preamble in front of + // the boundary. + continue + } + b.finished = true + return false, errors.WithMessagef(errNoBoundaryTerminator, "expecting boundary %q, got %q", string(b.prefix), string(line)) + } +} + +// isDelimiter returns true for --BOUNDARY\r\n but not --BOUNDARY-- +func (b *boundaryReader) isDelimiter(buf []byte) bool { + idx := bytes.Index(buf, b.prefix) + if idx == -1 { + return false + } + + // Fast forward to the end of the boundary prefix. + buf = buf[idx+len(b.prefix):] + if len(buf) > 0 { + if unicode.IsSpace(rune(buf[0])) { + return true + } + } + + return false +} + +// isTerminator returns true for --BOUNDARY-- +func (b *boundaryReader) isTerminator(buf []byte) bool { + idx := bytes.Index(buf, b.final) + return idx != -1 +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/builder.go b/vendor/github.com/jhillyerd/enmime/v2/builder.go new file mode 100644 index 000000000..5f6837790 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/builder.go @@ -0,0 +1,470 @@ +package enmime + +import ( + "bytes" + "errors" + "io" + "math/rand" + "mime" + "net/mail" + "net/textproto" + "os" + "path/filepath" + "reflect" + "time" + + "github.com/jhillyerd/enmime/v2/internal/stringutil" +) + +// MailBuilder facilitates the easy construction of a MIME message. Each manipulation method +// returns a copy of the receiver struct. It can be considered immutable if the caller does not +// modify the string and byte slices passed in. Immutability allows the headers or entire message +// to be reused across multiple threads. +type MailBuilder struct { + to, cc, bcc []mail.Address + from mail.Address + replyTo []mail.Address + subject string + date time.Time + header textproto.MIMEHeader + text, html []byte + inlines, attachments []*Part + err error + randSource rand.Source +} + +// Builder returns an empty MailBuilder struct. +func Builder() MailBuilder { + return MailBuilder{} +} + +// RandSeed sets the seed for random uuid boundary strings. +func (p MailBuilder) RandSeed(seed int64) MailBuilder { + p.randSource = stringutil.NewLockedSource(seed) + return p +} + +// Error returns the stored error from a file attachment/inline read or nil. +func (p MailBuilder) Error() error { + return p.err +} + +// Date returns a copy of MailBuilder with the specified Date header. +func (p MailBuilder) Date(date time.Time) MailBuilder { + p.date = date + return p +} + +// GetDate returns the stored date. +func (p *MailBuilder) GetDate() time.Time { + return p.date +} + +// From returns a copy of MailBuilder with the specified From header. +func (p MailBuilder) From(name, addr string) MailBuilder { + p.from = mail.Address{Name: name, Address: addr} + return p +} + +// GetFrom returns the stored from header. +func (p *MailBuilder) GetFrom() mail.Address { + return p.from +} + +// Subject returns a copy of MailBuilder with the specified Subject header. +func (p MailBuilder) Subject(subject string) MailBuilder { + p.subject = subject + return p +} + +// GetSubject returns the stored subject header. +func (p *MailBuilder) GetSubject() string { + return p.subject +} + +// To returns a copy of MailBuilder with this name & address appended to the To header. name may be +// empty. +func (p MailBuilder) To(name, addr string) MailBuilder { + if len(addr) > 0 { + p.to = append(p.to, mail.Address{Name: name, Address: addr}) + } + return p +} + +// ToAddrs returns a copy of MailBuilder with the specified To addresses. +func (p MailBuilder) ToAddrs(to []mail.Address) MailBuilder { + p.to = to + return p +} + +// GetTo returns a copy of the stored to addresses. +func (p *MailBuilder) GetTo() []mail.Address { + var to []mail.Address + to = append(to, p.to...) + return to +} + +// CC returns a copy of MailBuilder with this name & address appended to the CC header. name may be +// empty. +func (p MailBuilder) CC(name, addr string) MailBuilder { + if len(addr) > 0 { + p.cc = append(p.cc, mail.Address{Name: name, Address: addr}) + } + return p +} + +// CCAddrs returns a copy of MailBuilder with the specified CC addresses. +func (p MailBuilder) CCAddrs(cc []mail.Address) MailBuilder { + p.cc = cc + return p +} + +// GetCC returns a copy of the stored cc addresses. +func (p *MailBuilder) GetCC() []mail.Address { + var cc []mail.Address + cc = append(cc, p.cc...) + return cc +} + +// BCC returns a copy of MailBuilder with this name & address appended to the BCC list. name may be +// empty. This method only has an effect if the Send method is used to transmit the message, there +// is no effect on the parts returned by Build(). +func (p MailBuilder) BCC(name, addr string) MailBuilder { + if len(addr) > 0 { + p.bcc = append(p.bcc, mail.Address{Name: name, Address: addr}) + } + return p +} + +// BCCAddrs returns a copy of MailBuilder with the specified as the blind CC list. This method only +// has an effect if the Send method is used to transmit the message, there is no effect on the parts +// returned by Build(). +func (p MailBuilder) BCCAddrs(bcc []mail.Address) MailBuilder { + p.bcc = bcc + return p +} + +// GetBCC returns a copy of the stored bcc addresses. +func (p *MailBuilder) GetBCC() []mail.Address { + var bcc []mail.Address + bcc = append(bcc, p.bcc...) + return bcc +} + +// ReplyTo returns a copy of MailBuilder with this name & address appended to the To header. name +// may be empty. +func (p MailBuilder) ReplyTo(name, addr string) MailBuilder { + if len(addr) > 0 { + p.replyTo = append(p.replyTo, mail.Address{Name: name, Address: addr}) + } + return p +} + +// ReplyToAddrs returns a copy of MailBuilder with the new reply to header list. This method only +// has an effect if the Send method is used to transmit the message, there is no effect on the parts +// returned by Build(). +func (p MailBuilder) ReplyToAddrs(replyTo []mail.Address) MailBuilder { + p.replyTo = replyTo + return p +} + +// GetReplyTo returns a copy of the stored replyTo header addresses. +func (p *MailBuilder) GetReplyTo() []mail.Address { + replyTo := make([]mail.Address, len(p.replyTo)) + copy(replyTo, p.replyTo) + + return replyTo +} + +// Header returns a copy of MailBuilder with the specified value added to the named header. +func (p MailBuilder) Header(name, value string) MailBuilder { + // Copy existing header map + h := textproto.MIMEHeader{} + for k, v := range p.header { + h[k] = v + } + h.Add(name, value) + p.header = h + return p +} + +// GetHeader gets the first value associated with the given header. +func (p *MailBuilder) GetHeader(name string) string { + return p.header.Get(name) +} + +// Text returns a copy of MailBuilder that will use the provided bytes for its text/plain Part. +func (p MailBuilder) Text(body []byte) MailBuilder { + p.text = body + return p +} + +// GetText returns a copy of the stored text/plain part. +func (p *MailBuilder) GetText() []byte { + var text []byte + text = append(text, p.text...) + return text +} + +// HTML returns a copy of MailBuilder that will use the provided bytes for its text/html Part. +func (p MailBuilder) HTML(body []byte) MailBuilder { + p.html = body + return p +} + +// GetHTML returns a copy of the stored text/html part. +func (p *MailBuilder) GetHTML() []byte { + var html []byte + html = append(html, p.html...) + return html +} + +// AddAttachment returns a copy of MailBuilder that includes the specified attachment. +func (p MailBuilder) AddAttachment(b []byte, contentType string, fileName string) MailBuilder { + part := NewPart(contentType) + part.Content = b + part.FileName = fileName + part.Disposition = cdAttachment + p.attachments = append(p.attachments, part) + return p +} + +// AddAttachmentWithReader returns a copy of MailBuilder that includes the specified attachment, using an io.Reader to pull the content of the attachment. +func (p MailBuilder) AddAttachmentWithReader(r io.Reader, contentType string, fileName string) MailBuilder { + part := NewPart(contentType) + part.ContentReader = r + part.FileName = fileName + part.Disposition = cdAttachment + p.attachments = append(p.attachments, part) + return p +} + +// AddFileAttachment returns a copy of MailBuilder that includes the specified attachment. +// fileName, will be populated from the base name of path. Content type will be detected from the +// path extension. +func (p MailBuilder) AddFileAttachment(path string) MailBuilder { + // Only allow first p.err value + if p.err != nil { + return p + } + b, err := os.ReadFile(path) + if err != nil { + p.err = err + return p + } + name := filepath.Base(path) + ctype := mime.TypeByExtension(filepath.Ext(name)) + return p.AddAttachment(b, ctype, name) +} + +// AddInline returns a copy of MailBuilder that includes the specified inline. fileName and +// contentID may be left empty. +func (p MailBuilder) AddInline( + b []byte, + contentType string, + fileName string, + contentID string, +) MailBuilder { + part := NewPart(contentType) + part.Content = b + part.FileName = fileName + part.Disposition = cdInline + part.ContentID = contentID + p.inlines = append(p.inlines, part) + return p +} + +// AddFileInline returns a copy of MailBuilder that includes the specified inline. fileName and +// contentID will be populated from the base name of path. Content type will be detected from the +// path extension. +func (p MailBuilder) AddFileInline(path string) MailBuilder { + // Only allow first p.err value + if p.err != nil { + return p + } + b, err := os.ReadFile(path) + if err != nil { + p.err = err + return p + } + name := filepath.Base(path) + ctype := mime.TypeByExtension(filepath.Ext(name)) + return p.AddInline(b, ctype, name, name) +} + +// AddOtherPart returns a copy of MailBuilder that includes the specified embedded part. +// fileName may be left empty. +// It's useful when you want to embed image with CID. +func (p MailBuilder) AddOtherPart( + b []byte, + contentType string, + fileName string, + contentID string, +) MailBuilder { + part := NewPart(contentType) + part.Content = b + part.FileName = fileName + part.ContentID = contentID + p.inlines = append(p.inlines, part) + return p +} + +// AddFileOtherPart returns a copy of MailBuilder that includes the specified other part. +// Filename and contentID will be populated from the base name of path. +// Content type will be detected from the path extension. +func (p MailBuilder) AddFileOtherPart(path string) MailBuilder { + // Only allow first p.err value + if p.err != nil { + return p + } + b, err := os.ReadFile(path) + if err != nil { + p.err = err + return p + } + name := filepath.Base(path) + ctype := mime.TypeByExtension(filepath.Ext(name)) + return p.AddOtherPart(b, ctype, name, name) +} + +// Build performs some basic validations, then constructs a tree of Part structs from the configured +// MailBuilder. It will set the Date header to now if it was not explicitly set. +func (p MailBuilder) Build() (*Part, error) { + if p.err != nil { + return nil, p.err + } + // Validations + if p.from.Address == "" { + return nil, errors.New("from not set") + } + if len(p.to)+len(p.cc)+len(p.bcc) == 0 { + return nil, errors.New(ErrorMissingRecipient) + } + // Fully loaded structure; the presence of text, html, inlines, and attachments will determine + // how much is necessary: + // + // multipart/mixed + // |- multipart/related + // | |- multipart/alternative + // | | |- text/plain + // | | `- text/html + // | |- other parts.. + // | `- inlines.. + // `- attachments.. + // + // We build this tree starting at the leaves, re-rooting as needed. + var root, part *Part + if p.text != nil || p.html == nil { + root = NewPart(ctTextPlain) + root.Content = p.text + root.Charset = utf8 + } + if p.html != nil { + part = NewPart(ctTextHTML) + part.Content = p.html + part.Charset = utf8 + if root == nil { + root = part + } else { + root.NextSibling = part + } + } + if p.text != nil && p.html != nil { + // Wrap Text & HTML bodies + part = root + root = NewPart(ctMultipartAltern) + root.AddChild(part) + } + if len(p.inlines) > 0 { + part = root + root = NewPart(ctMultipartRelated) + root.AddChild(part) + for _, ip := range p.inlines { + // Copy inline/other part to isolate mutations + part = &Part{} + *part = *ip + part.Header = make(textproto.MIMEHeader) + root.AddChild(part) + } + } + if len(p.attachments) > 0 { + part = root + root = NewPart(ctMultipartMixed) + root.AddChild(part) + for _, ap := range p.attachments { + // Copy attachment Part to isolate mutations + part = &Part{} + *part = *ap + part.Header = make(textproto.MIMEHeader) + root.AddChild(part) + } + } + // Headers + h := root.Header + h.Set(hnMIMEVersion, "1.0") + h.Set("From", p.from.String()) + h.Set("Subject", p.subject) + if len(p.to) > 0 { + h.Set("To", stringutil.JoinAddress(p.to)) + } + if len(p.cc) > 0 { + h.Set("Cc", stringutil.JoinAddress(p.cc)) + } + if len(p.replyTo) > 0 { + h.Set("Reply-To", stringutil.JoinAddress(p.replyTo)) + } + date := p.date + if date.IsZero() { + date = time.Now() + } + h.Set("Date", date.Format(time.RFC1123Z)) + for k, v := range p.header { + for _, s := range v { + h.Add(k, s) + } + } + if r := p.randSource; r != nil { + // Traverse all parts, discard match result. + _ = root.DepthMatchAll(func(part *Part) bool { + part.randSource = r + return false + }) + } + return root, nil +} + +// SendWithReversePath encodes the message and sends it via the specified Sender. +func (p MailBuilder) SendWithReversePath(sender Sender, from string) error { + buf := &bytes.Buffer{} + root, err := p.Build() + if err != nil { + return err + } + err = root.Encode(buf) + if err != nil { + return err + } + recips := make([]string, 0, len(p.to)+len(p.cc)+len(p.bcc)) + for _, a := range p.to { + recips = append(recips, a.Address) + } + for _, a := range p.cc { + recips = append(recips, a.Address) + } + for _, a := range p.bcc { + recips = append(recips, a.Address) + } + return sender.Send(from, recips, buf.Bytes()) +} + +// Send encodes the message and sends it via the specified Sender, using the address provided to +// `From()` as the reverse-path. +func (p MailBuilder) Send(sender Sender) error { + return p.SendWithReversePath(sender, p.from.Address) +} + +// Equals uses the reflect package to test two MailBuilder structs for equality, primarily for unit +// tests. +func (p MailBuilder) Equals(o MailBuilder) bool { + return reflect.DeepEqual(p, o) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/detect.go b/vendor/github.com/jhillyerd/enmime/v2/detect.go new file mode 100644 index 000000000..e7aacfa48 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/detect.go @@ -0,0 +1,94 @@ +package enmime + +import ( + "strings" + + inttp "github.com/jhillyerd/enmime/v2/internal/textproto" +) + +// detectMultipartMessage returns true if the message has a recognized multipart Content-Type header +func detectMultipartMessage(root *Part, multipartWOBoundaryAsSinglepart bool) bool { + // Parse top-level multipart + ctype := root.Header.Get(hnContentType) + mtype, params, _, err := root.parseMediaType(ctype) + if err != nil { + return false + } + + if boundary := params[hpBoundary]; multipartWOBoundaryAsSinglepart && boundary == "" { + return false + } + + // According to rfc2046#section-5.1.7 all other multipart should + // be treated as multipart/mixed + return strings.HasPrefix(mtype, ctMultipartPrefix) +} + +// detectAttachmentHeader returns true, if the given header defines an attachment. First it checks +// if the Content-Disposition header defines either an attachment part or an inline part with at +// least one parameter. If this test is false, the Content-Type header is checked for attachment, +// but not inline. Email clients use inline for their text bodies. +// +// Valid Attachment-Headers: +// +// - Content-Disposition: attachment; filename="frog.jpg" +// - Content-Disposition: inline; filename="frog.jpg" +// - Content-Type: attachment; filename="frog.jpg" +func detectAttachmentHeader(root *Part, header inttp.MIMEHeader) bool { + mtype, params, _, _ := root.parseMediaType(header.Get(hnContentDisposition)) + if strings.ToLower(mtype) == cdAttachment || + (strings.ToLower(mtype) == cdInline && len(params) > 0) { + return true + } + + mtype, _, _, _ = root.parseMediaType(header.Get(hnContentType)) + return strings.ToLower(mtype) == cdAttachment +} + +// detectTextHeader returns true, if the the MIME headers define a valid 'text/plain' or 'text/html' +// part. If the emptyContentTypeIsPlain argument is set to true, a missing Content-Type header will +// result in a positive plain part detection. +func detectTextHeader(root *Part, header inttp.MIMEHeader, emptyContentTypeIsText bool) bool { + ctype := header.Get(hnContentType) + if ctype == "" && emptyContentTypeIsText { + return true + } + + if mtype, _, _, err := root.parseMediaType(ctype); err == nil { + switch mtype { + case ctTextPlain, ctTextHTML: + return true + } + } + + return false +} + +// detectBinaryBody returns true if the mail header defines a binary body. +func detectBinaryBody(root *Part) bool { + header := inttp.MIMEHeader(root.Header) // Use internal header methods. + if detectTextHeader(root, header, true) { + // It is text/plain, but an attachment. + // Content-Type: text/plain; name="test.csv" + // Content-Disposition: attachment; filename="test.csv" + // Check for attachment only, or inline body is marked + // as attachment, too. + mtype, _, _, _ := root.parseMediaType(header.Get(hnContentDisposition)) + return strings.ToLower(mtype) == cdAttachment + } + + isBin := detectAttachmentHeader(root, header) + if !isBin { + // This must be an attachment, if the Content-Type is not + // 'text/plain' or 'text/html'. + // Example: + // Content-Type: application/pdf; name="doc.pdf" + mtype, _, _, _ := root.parseMediaType(header.Get(hnContentType)) + mtype = strings.ToLower(mtype) + if mtype != ctTextPlain && mtype != ctTextHTML { + return true + } + } + + return isBin +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/encode.go b/vendor/github.com/jhillyerd/enmime/v2/encode.go new file mode 100644 index 000000000..bef657b0e --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/encode.go @@ -0,0 +1,340 @@ +package enmime + +import ( + "bufio" + "encoding/base64" + "io" + "mime" + "mime/quotedprintable" + "net/textproto" + "sort" + "strings" + "time" + + "github.com/jhillyerd/enmime/v2/internal/coding" + "github.com/jhillyerd/enmime/v2/internal/stringutil" +) + +// b64Percent determines the percent of non-ASCII characters enmime will tolerate before switching +// from quoted-printable to base64 encoding. +const b64Percent = 20 + +type transferEncoding byte + +const ( + te7Bit transferEncoding = iota + te8Bit + teQuoted + teBase64 + teRaw +) + +const ( + base64EncodedLineLen = 76 + base64DecodedLineLen = base64EncodedLineLen * 3 / 4 // this is ok since lineLen is divisible by 4 + linesPerChunk = 128 + readChunkSize = base64DecodedLineLen * linesPerChunk +) + +var crnl = []byte{'\r', '\n'} + +// Encode writes this Part and all its children to the specified writer in MIME format. +func (p *Part) Encode(writer io.Writer) error { + if p.Header == nil { + p.Header = make(textproto.MIMEHeader) + } + if p.ContentReader != nil { + // read some data in order to check whether the content is empty + p.Content = make([]byte, readChunkSize) + n, err := p.ContentReader.Read(p.Content) + if err != nil && err != io.EOF { + return err + } + p.Content = p.Content[:n] + } + cte := teRaw + if p.parser == nil || !p.parser.rawContent { + cte = p.setupMIMEHeaders() + } + // Encode this part. + b := bufio.NewWriter(writer) + if err := p.encodeHeader(b); err != nil { + return err + } + if len(p.Content) > 0 { + if _, err := b.Write(crnl); err != nil { + return err + } + if err := p.encodeContent(b, cte); err != nil { + return err + } + } + if p.FirstChild == nil { + return b.Flush() + } + // Encode children. + endMarker := []byte("\r\n--" + p.Boundary + "--") + marker := endMarker[:len(endMarker)-2] + c := p.FirstChild + for c != nil { + if _, err := b.Write(marker); err != nil { + return err + } + if _, err := b.Write(crnl); err != nil { + return err + } + if err := c.Encode(b); err != nil { + return err + } + c = c.NextSibling + } + if _, err := b.Write(endMarker); err != nil { + return err + } + if _, err := b.Write(crnl); err != nil { + return err + } + return b.Flush() +} + +// setupMIMEHeaders determines content transfer encoding, generates a boundary string if required, +// then sets the Content-Type (type, charset, filename, boundary) and Content-Disposition headers. +func (p *Part) setupMIMEHeaders() transferEncoding { + // Determine content transfer encoding. + + // If we are encoding a part that previously had content-transfer-encoding set, unset it so + // the correct encoding detection can be done below. + p.Header.Del(hnContentEncoding) + + cte := te7Bit + if len(p.Content) > 0 { + if strings.Index(strings.ToLower(p.ContentType), "message/") == 0 { + // RFC 1341: `message` types must have no encoding other than "7bit", "8bit", or + // "binary". The message header fields are always US-ASCII in any case, and data within + // the body can still be encoded, in which case the Content-Transfer-Encoding header + // field in the encapsulated message will reflect this. + cte = te8Bit + } else { + cte = teBase64 + if p.TextContent() && p.ContentReader == nil { + cte = p.selectTransferEncoding(p.Content, false) + if p.Charset == "" { + p.Charset = utf8 + } + } + } + + // RFC 2045: 7bit is assumed if CTE header not present. + switch cte { + case te8Bit: + p.Header.Set(hnContentEncoding, cte8Bit) + case teBase64: + p.Header.Set(hnContentEncoding, cteBase64) + case teQuoted: + p.Header.Set(hnContentEncoding, cteQuotedPrintable) + } + } + + // Setup headers. + if p.FirstChild != nil && p.Boundary == "" { + // Multipart, generate random boundary marker. + p.Boundary = "enmime-" + stringutil.UUID(p.randSource) + } + if p.ContentID != "" { + p.Header.Set(hnContentID, coding.ToIDHeader(p.ContentID)) + } + fileName := p.FileName + switch p.selectTransferEncoding([]byte(p.FileName), true) { + case teBase64: + fileName = mime.BEncoding.Encode(utf8, p.FileName) + case teQuoted: + fileName = mime.QEncoding.Encode(utf8, p.FileName) + } + + if p.ContentType != "" { + // Build content type header. + param := make(map[string]string) + for k, v := range p.ContentTypeParams { + param[k] = v + } + setParamValue(param, hpCharset, p.Charset) + setParamValue(param, hpName, fileName) + setParamValue(param, hpBoundary, p.Boundary) + if mt := mime.FormatMediaType(p.ContentType, param); mt != "" { + p.ContentType = mt + } + p.Header.Set(hnContentType, p.ContentType) + } + + if p.Disposition != "" { + // Build disposition header. + param := make(map[string]string) + setParamValue(param, hpFilename, fileName) + if !p.FileModDate.IsZero() { + setParamValue(param, hpModDate, p.FileModDate.UTC().Format(time.RFC822)) + } + if mt := mime.FormatMediaType(p.Disposition, param); mt != "" { + p.Disposition = mt + } + p.Header.Set(hnContentDisposition, p.Disposition) + } + + return cte +} + +// encodeHeader writes out a sorted list of headers. +func (p *Part) encodeHeader(b *bufio.Writer) error { + keys := make([]string, 0, len(p.Header)) + for k := range p.Header { + keys = append(keys, k) + } + rawContent := p.parser != nil && p.parser.rawContent + + sort.Strings(keys) + for _, k := range keys { + for _, v := range p.Header[k] { + encv := v + if !rawContent { + switch p.selectTransferEncoding([]byte(v), true) { + case teBase64: + encv = mime.BEncoding.Encode(utf8, v) + case teQuoted: + encv = mime.QEncoding.Encode(utf8, v) + } + } + // _ used to prevent early wrapping + wb := stringutil.Wrap(76, k, ":_", encv, "\r\n") + wb[len(k)+1] = ' ' + if _, err := b.Write(wb); err != nil { + return err + } + } + } + return nil +} + +// encodeContent writes out the content in the selected encoding. +func (p *Part) encodeContent(b *bufio.Writer, cte transferEncoding) (err error) { + if p.ContentReader != nil { + return p.encodeContentFromReader(b) + } + + if p.parser != nil && p.parser.rawContent { + cte = teRaw + } + + switch cte { + case teBase64: + enc := base64.StdEncoding + text := make([]byte, enc.EncodedLen(len(p.Content))) + enc.Encode(text, p.Content) + // Wrap lines. + lineLen := 76 + for len(text) > 0 { + if lineLen > len(text) { + lineLen = len(text) + } + if _, err = b.Write(text[:lineLen]); err != nil { + return err + } + if _, err := b.Write(crnl); err != nil { + return err + } + text = text[lineLen:] + } + case teQuoted: + qp := quotedprintable.NewWriter(b) + if _, err = qp.Write(p.Content); err != nil { + return err + } + err = qp.Close() + default: + _, err = b.Write(p.Content) + } + return err +} + +// encodeContentFromReader writes out the content read from the reader using base64 encoding. +func (p *Part) encodeContentFromReader(b *bufio.Writer) error { + text := make([]byte, base64EncodedLineLen) // a single base64 encoded line + enc := base64.StdEncoding + + chunk := make([]byte, readChunkSize) // contains a whole number of lines + copy(chunk, p.Content) // copy the data of the initial read that was issued by `Encode` + n := len(p.Content) + + for { + // call read until we get a full chunk / error + for n < len(chunk) { + c, err := p.ContentReader.Read(chunk[n:]) + if err != nil { + if err == io.EOF { + break + } + return err + } + + n += c + } + + for i := 0; i < n; i += base64DecodedLineLen { + size := n - i + if size > base64DecodedLineLen { + size = base64DecodedLineLen + } + + enc.Encode(text, chunk[i:i+size]) + if _, err := b.Write(text[:enc.EncodedLen(size)]); err != nil { + return err + } + if _, err := b.Write(crnl); err != nil { + return err + } + } + + if n < len(chunk) { + break + } + + n = 0 + } + + return nil +} + +// selectTransferEncoding scans content for non-ASCII characters and selects 'b' or 'q' encoding. +func (p *Part) selectTransferEncoding(content []byte, quoteLineBreaks bool) transferEncoding { + if len(content) == 0 { + return te7Bit + } + + if p.encoder != nil && p.encoder.forceQuotedPrintableCteOption { + return teQuoted + } + + // Binary chars remaining before we choose b64 encoding. + threshold := b64Percent * len(content) / 100 + bincount := 0 + for _, b := range content { + if (b < ' ' || '~' < b) && b != '\t' { + if !quoteLineBreaks && (b == '\r' || b == '\n') { + continue + } + bincount++ + if bincount >= threshold { + return teBase64 + } + } + } + if bincount == 0 { + return te7Bit + } + return teQuoted +} + +// setParamValue will ignore empty values +func setParamValue(p map[string]string, k, v string) { + if v != "" { + p[k] = v + } +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/encoder_options.go b/vendor/github.com/jhillyerd/enmime/v2/encoder_options.go new file mode 100644 index 000000000..e777903a2 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/encoder_options.go @@ -0,0 +1,33 @@ +package enmime + +type EncoderOption interface { + apply(p *Encoder) +} + +// Encoder implements MIME part encoding options +type Encoder struct { + forceQuotedPrintableCteOption bool +} + +// ForceQuotedPrintableCte forces "quoted-printable" transfer encoding when selecting Content Transfer Encoding, preventing the use of base64. +func ForceQuotedPrintableCte(b bool) EncoderOption { + return forceQuotedPrintableCteOption(b) +} + +type forceQuotedPrintableCteOption bool + +func (o forceQuotedPrintableCteOption) apply(p *Encoder) { + p.forceQuotedPrintableCteOption = bool(o) +} + +func NewEncoder(ops ...EncoderOption) *Encoder { + e := Encoder{ + forceQuotedPrintableCteOption: false, + } + + for _, o := range ops { + o.apply(&e) + } + + return &e +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/enmime.go b/vendor/github.com/jhillyerd/enmime/v2/enmime.go new file mode 100644 index 000000000..c71fc822b --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/enmime.go @@ -0,0 +1,57 @@ +// Package enmime implements a MIME encoding and decoding library. It's built on top of Go's +// included mime/multipart support where possible, but is geared towards parsing MIME encoded +// emails. +// +// # Overview +// +// The enmime API has two conceptual layers. The lower layer is a tree of Part structs, +// representing each component of a decoded MIME message. The upper layer, called an Envelope +// provides an intuitive way to interact with a MIME message. +// +// # Part Tree +// +// Calling ReadParts causes enmime to parse the body of a MIME message into a tree of Part objects, +// each of which is aware of its content type, filename and headers. The content of a Part is +// available as a slice of bytes via the Content field. +// +// If the part was encoded in quoted-printable or base64, it is decoded prior to being placed in +// Content. If the Part contains text in a character set other than utf-8, enmime will attempt to +// convert it to utf-8. +// +// To locate a particular Part, pass a custom PartMatcher function into the BreadthMatchFirst() or +// DepthMatchFirst() methods to search the Part tree. BreadthMatchAll() and DepthMatchAll() will +// collect all Parts matching your criteria. +// +// # Envelope +// +// ReadEnvelope returns an Envelope struct. Behind the scenes a Part tree is constructed, and then +// sorted into the correct fields of the Envelope. +// +// The Envelope contains both the plain text and HTML portions of the email. If there was no plain +// text Part available, the HTML Part will be down-converted using the html2text library[1]. The +// root of the Part tree, as well as slices of the inline and attachment Parts are also available. +// +// # Headers +// +// Every MIME Part has its own headers, accessible via the Part.Header field. The raw headers for +// an Envelope are available in Root.Header. Envelope also provides helper methods to fetch +// headers: GetHeader(key) will return the RFC 2047 decoded value of the specified header. +// AddressList(key) will convert the specified address header into a slice of net/mail.Address +// values. +// +// # Errors +// +// enmime attempts to be tolerant of poorly encoded MIME messages. In situations where parsing is +// not possible, the ReadEnvelope and ReadParts functions will return a hard error. If enmime is +// able to continue parsing the message, it will add an entry to the Errors slice on the relevant +// Part. After parsing is complete, all Part errors will be appended to the Envelope Errors slice. +// The Error* constants can be used to identify a specific class of error. +// +// Please note that enmime parses messages into memory, so it is not likely to perform well with +// multi-gigabyte attachments. +// +// enmime is open source software released under the MIT License. The latest version can be found +// at https://github.com/jhillyerd/enmime/v2 +// +// [1]: https://github.com/inbucket/html2text +package enmime // import "github.com/jhillyerd/enmime/v2" diff --git a/vendor/github.com/jhillyerd/enmime/v2/envelope.go b/vendor/github.com/jhillyerd/enmime/v2/envelope.go new file mode 100644 index 000000000..9bf720f16 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/envelope.go @@ -0,0 +1,336 @@ +package enmime + +import ( + "fmt" + "io" + "mime" + "net/mail" + "net/textproto" + "strings" + "time" + + "github.com/inbucket/html2text" + "github.com/jhillyerd/enmime/v2/internal/coding" + inttp "github.com/jhillyerd/enmime/v2/internal/textproto" + "github.com/pkg/errors" +) + +// Envelope is a simplified wrapper for MIME email messages. +type Envelope struct { + Text string // The plain text portion of the message + HTML string // The HTML portion of the message + Root *Part // The top-level Part + Attachments []*Part // All parts having a Content-Disposition of attachment + Inlines []*Part // All parts having a Content-Disposition of inline + // All non-text parts that were not placed in Attachments or Inlines, such as multipart/related + // content. + OtherParts []*Part + Errors []*Error // Errors encountered while parsing + header *textproto.MIMEHeader // Header from original message +} + +// GetHeaderKeys returns a list of header keys seen in this message. Get +// individual headers with `GetHeader(name)` +func (e *Envelope) GetHeaderKeys() (headers []string) { + if e.header == nil { + return + } + for key := range *e.header { + headers = append(headers, key) + } + return headers +} + +// GetHeader processes the specified header for RFC 2047 encoded words and returns the result as a +// UTF-8 string +func (e *Envelope) GetHeader(name string) string { + if e.header == nil { + return "" + } + return coding.DecodeExtHeader(e.header.Get(name)) +} + +// GetHeaderValues processes the specified header for RFC 2047 encoded words and returns all existing +// values as a list of UTF-8 strings +func (e *Envelope) GetHeaderValues(name string) []string { + if e.header == nil { + return []string{} + } + + rawValues := (*e.header)[inttp.CanonicalEmailMIMEHeaderKey(name)] + values := make([]string, 0, len(rawValues)) + for _, v := range rawValues { + values = append(values, coding.DecodeExtHeader(v)) + } + return values +} + +// SetHeader sets given header name to the given value. +// If the header exists already, all existing values are replaced. +func (e *Envelope) SetHeader(name string, value []string) error { + if name == "" { + return errors.New("provide non-empty header name") + } + + for i, v := range value { + if i == 0 { + e.header.Set(name, mime.BEncoding.Encode("utf-8", v)) + continue + } + e.header.Add(name, mime.BEncoding.Encode("utf-8", v)) + } + return nil +} + +// AddHeader appends given header value to header name without changing existing values. +// If the header does not exist already, it will be created. +func (e *Envelope) AddHeader(name string, value string) error { + if name == "" { + return errors.New("provide non-empty header name") + } + + e.header.Add(name, mime.BEncoding.Encode("utf-8", value)) + return nil +} + +// DeleteHeader deletes given header. +func (e *Envelope) DeleteHeader(name string) error { + if name == "" { + return errors.New("provide non-empty header name") + } + + e.header.Del(name) + return nil +} + +// AddressList returns a mail.Address slice with RFC 2047 encoded names converted to UTF-8 +func (e *Envelope) AddressList(key string) ([]*mail.Address, error) { + if e.header == nil { + return nil, errors.New("no headers available") + } + if !AddressHeaders[strings.ToLower(key)] { + return nil, fmt.Errorf("%s is not an address header", key) + } + + return ParseAddressList(e.header.Get(key)) +} + +// Date parses the Date header field. +func (e *Envelope) Date() (time.Time, error) { + hdr := e.GetHeader("Date") + if hdr == "" { + return time.Time{}, mail.ErrHeaderNotPresent + } + return mail.ParseDate(hdr) +} + +// Clone returns a clone of the current Envelope +func (e *Envelope) Clone() *Envelope { + if e == nil { + return nil + } + + newEnvelope := &Envelope{ + e.Text, + e.HTML, + e.Root.Clone(nil), + e.Attachments, + e.Inlines, + e.OtherParts, + e.Errors, + e.header, + } + return newEnvelope +} + +// ReadEnvelope is a wrapper around ReadParts and EnvelopeFromPart. It parses the content of the +// provided reader into an Envelope, downconverting HTML to plain text if needed, and sorting the +// attachments, inlines and other parts into their respective slices. Errors are collected from all +// Parts and placed into the Envelope.Errors slice. +// Uses default parser. +func ReadEnvelope(r io.Reader) (*Envelope, error) { + return defaultParser.ReadEnvelope(r) +} + +// ReadEnvelope is the same as ReadEnvelope, but respects parser configurations. +func (p Parser) ReadEnvelope(r io.Reader) (*Envelope, error) { + // Read MIME parts from reader + root, err := p.ReadParts(r) + if err != nil { + return nil, errors.WithMessage(err, "Failed to ReadParts") + } + return p.EnvelopeFromPart(root) +} + +// EnvelopeFromPart uses the provided Part tree to build an Envelope, downconverting HTML to plain +// text if needed, and sorting the attachments, inlines and other parts into their respective +// slices. Errors are collected from all Parts and placed into the Envelopes Errors slice. +func EnvelopeFromPart(root *Part) (*Envelope, error) { + return defaultParser.EnvelopeFromPart(root) +} + +// EnvelopeFromPart is the same as EnvelopeFromPart, but respects parser configurations. +func (p Parser) EnvelopeFromPart(root *Part) (*Envelope, error) { + e := &Envelope{ + Root: root, + header: &root.Header, + } + + if detectMultipartMessage(root, p.multipartWOBoundaryAsSinglePart) { + // Multi-part message (message with attachments, etc) + if err := parseMultiPartBody(root, e); err != nil { + return nil, err + } + } else { + if detectBinaryBody(root) { + // Attachment only, no text + if root.Disposition == cdInline { + e.Inlines = append(e.Inlines, root) + } else { + e.Attachments = append(e.Attachments, root) + } + } else { + // Only text, no attachments + parseTextOnlyBody(root, e) + } + } + + // Down-convert HTML to text if necessary + if e.Text == "" && e.HTML != "" { + // We always warn when this happens + e.Root.addWarning(ErrorPlainTextFromHTML, "Message did not contain a text/plain part") + + if !p.disableTextConversion { + var err error + if e.Text, err = html2text.FromString(e.HTML); err != nil { + e.Text = "" // Down-conversion shouldn't fail + p := e.Root.BreadthMatchFirst(matchHTMLBodyPart) + p.addErrorf(ErrorPlainTextFromHTML, "Failed to downconvert HTML: %v", err) + } + } + } + + // Copy part errors into Envelope. + if e.Root != nil { + _ = e.Root.DepthMatchAll(func(part *Part) bool { + // Using DepthMatchAll to traverse all parts, don't care about result. + e.Errors = append(e.Errors, part.Errors...) + return false + }) + } + + return e, nil +} + +// parseTextOnlyBody parses a plain text message in root that has MIME-like headers, but +// only contains a single part - no boundaries, etc. The result is placed in e. +func parseTextOnlyBody(root *Part, e *Envelope) { + // Determine character set + var charset string + var isHTML bool + if ctype := root.Header.Get(hnContentType); ctype != "" { + if mediatype, mparams, _, err := root.parseMediaType(ctype); err == nil { + isHTML = (mediatype == ctTextHTML) + if mparams[hpCharset] != "" { + charset = mparams[hpCharset] + } + } + } + + // Read transcoded text + if isHTML { + rawHTML := string(root.Content) + // Note: Empty e.Text will trigger html2text conversion + e.HTML = rawHTML + if charset == "" { + // Search for charset in HTML metadata + if charset = coding.FindCharsetInHTML(rawHTML); charset != "" { + // Found charset in HTML + if convHTML, err := coding.ConvertToUTF8String(charset, root.Content); err == nil { + // Successful conversion + e.HTML = convHTML + } else { + // Conversion failed + root.addWarning(ErrorCharsetConversion, err.Error()) + } + } + } + } else { + e.Text = string(root.Content) + } +} + +// parseMultiPartBody parses a multipart message in root. The result is placed in e. +func parseMultiPartBody(root *Part, e *Envelope) error { + // Parse top-level multipart + ctype := root.Header.Get(hnContentType) + mediatype, params, _, err := root.parseMediaType(ctype) + if err != nil { + return fmt.Errorf("unable to parse media type: %v", err) + } + if !strings.HasPrefix(mediatype, ctMultipartPrefix) { + return fmt.Errorf("unknown mediatype: %v", mediatype) + } + boundary := params[hpBoundary] + if boundary == "" { + return errors.New("unable to locate boundary param in Content-Type header") + } + + // Locate text body + if mediatype == ctMultipartAltern { + p := root.BreadthMatchFirst(func(p *Part) bool { + return p.ContentType == ctTextPlain && p.Disposition != cdAttachment + }) + if p != nil { + e.Text = string(p.Content) + } + } else { + // multipart is of a mixed type + parts := root.DepthMatchAll(func(p *Part) bool { + return p.ContentType == ctTextPlain && p.Disposition != cdAttachment + }) + for i, p := range parts { + if i > 0 { + e.Text += "\n--\n" + } + e.Text += string(p.Content) + } + } + + // Locate HTML body + p := root.DepthMatchFirst(matchHTMLBodyPart) + if p != nil { + e.HTML += string(p.Content) + } + + // Locate attachments + e.Attachments = root.BreadthMatchAll(func(p *Part) bool { + return p.Disposition == cdAttachment || p.ContentType == ctAppOctetStream + }) + + // Locate inlines + e.Inlines = root.BreadthMatchAll(func(p *Part) bool { + return p.Disposition == cdInline && !strings.HasPrefix(p.ContentType, ctMultipartPrefix) + }) + + // Locate others parts not considered in attachments or inlines + e.OtherParts = root.BreadthMatchAll(func(p *Part) bool { + if strings.HasPrefix(p.ContentType, ctMultipartPrefix) { + return false + } + if p.Disposition != "" { + return false + } + if p.ContentType == ctAppOctetStream { + return false + } + return p.ContentType != ctTextPlain && p.ContentType != ctTextHTML + }) + + return nil +} + +// Used by Part matchers to locate the HTML body. Not inlined because it's used in multiple places. +func matchHTMLBodyPart(p *Part) bool { + return p.ContentType == ctTextHTML && p.Disposition != cdAttachment +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/error.go b/vendor/github.com/jhillyerd/enmime/v2/error.go new file mode 100644 index 000000000..0f16ae6fc --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/error.go @@ -0,0 +1,104 @@ +package enmime + +import ( + "fmt" +) + +const ( + // ErrorMalformedBase64 name. + ErrorMalformedBase64 = "Malformed Base64" + // ErrorMalformedHeader name. + ErrorMalformedHeader = "Malformed Header" + // ErrorMissingBoundary name. + ErrorMissingBoundary = "Missing Boundary" + // ErrorMissingContentType name. + ErrorMissingContentType = "Missing Content-Type" + // ErrorCharsetConversion name. + ErrorCharsetConversion = "Character Set Conversion" + // ErrorContentEncoding name. + ErrorContentEncoding = "Content Encoding" + // ErrorPlainTextFromHTML name. + ErrorPlainTextFromHTML = "Plain Text from HTML" + // ErrorCharsetDeclaration name. + ErrorCharsetDeclaration = "Character Set Declaration Mismatch" + // ErrorMissingRecipient name. + ErrorMissingRecipient = "no recipients (to, cc, bcc) set" + // ErrorMalformedChildPart name. + ErrorMalformedChildPart = "Malformed child part" +) + +// Error describes an error encountered while parsing. +type Error struct { + Name string // The name or type of error encountered, from Error consts. + Detail string // Additional detail about the cause of the error, if available. + Severe bool // Indicates that a portion of the message was lost during parsing. +} + +// Error formats the enmime.Error as a string. +func (e *Error) Error() string { + sev := "W" + if e.Severe { + sev = "E" + } + return fmt.Sprintf("[%s] %s: %s", sev, e.Name, e.Detail) +} + +// addError builds a severe Error and appends to the Part error slice. +func (p *Part) addError(name string, detail string) { + p.addProblem(&Error{name, detail, true}) +} + +// addErrorf builds a severe Error and appends to the Part error slice. +func (p *Part) addErrorf(name string, detailFmt string, args ...interface{}) { + p.addProblem(&Error{ + name, + fmt.Sprintf(detailFmt, args...), + true, + }) +} + +// addWarning builds a non-severe Error and appends to the Part error slice. +func (p *Part) addWarning(name string, detail string) { + p.addProblem(&Error{name, detail, false}) +} + +// addWarningf builds a non-severe Error and appends to the Part error slice. +func (p *Part) addWarningf(name string, detailFmt string, args ...interface{}) { + p.addProblem(&Error{ + name, + fmt.Sprintf(detailFmt, args...), + false, + }) +} + +// addProblem adds general *Error to the Part error slice. +func (p *Part) addProblem(err *Error) { + maxErrors := 0 + if p.parser != nil { + // Override global var. + maxErrors = p.parser.maxStoredPartErrors + } + + if (maxErrors == 0) || (len(p.Errors) < maxErrors) { + p.Errors = append(p.Errors, err) + } +} + +// ErrorCollector is an interface for collecting errors and warnings during +// parsing. +type ErrorCollector interface { + AddError(name string, detailFmt string, args ...any) + AddWarning(name string, detailFmt string, args ...any) +} + +type partErrorCollector struct { + part *Part +} + +func (p *partErrorCollector) AddError(name string, detailFmt string, args ...any) { + p.part.addErrorf(name, detailFmt, args...) +} + +func (p *partErrorCollector) AddWarning(name string, detailFmt string, args ...any) { + p.part.addWarningf(name, detailFmt, args...) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/flake.lock b/vendor/github.com/jhillyerd/enmime/v2/flake.lock new file mode 100644 index 000000000..17cec36a8 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1736657626, + "narHash": "sha256-FWlPMUzp0lkQBdhKlPqtQdqmp+/C+1MBiEytaYfrCTY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2f9e2f85cb14a46410a1399aa9ea7ecf433e422e", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/flake.nix b/vendor/github.com/jhillyerd/enmime/v2/flake.nix new file mode 100644 index 000000000..6ed09c305 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/flake.nix @@ -0,0 +1,17 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShell = pkgs.callPackage ./shell.nix { }; + } + ); +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/header.go b/vendor/github.com/jhillyerd/enmime/v2/header.go new file mode 100644 index 000000000..01c5c625c --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/header.go @@ -0,0 +1,244 @@ +package enmime + +import ( + "bufio" + "bytes" + "fmt" + "mime" + "net/mail" + "net/textproto" + "strings" + + "github.com/jhillyerd/enmime/v2/internal/coding" + "github.com/jhillyerd/enmime/v2/internal/stringutil" + inttp "github.com/jhillyerd/enmime/v2/internal/textproto" + + "github.com/pkg/errors" +) + +const ( + // Standard MIME content dispositions + cdAttachment = "attachment" + cdInline = "inline" + + // Standard MIME content types + ctAppOctetStream = "application/octet-stream" + ctMultipartAltern = "multipart/alternative" + ctMultipartMixed = "multipart/mixed" + ctMultipartPrefix = "multipart/" + ctMultipartRelated = "multipart/related" + ctTextPlain = "text/plain" + ctTextHTML = "text/html" + + // Standard Transfer encodings + cte7Bit = "7bit" + cte8Bit = "8bit" + cteBase64 = "base64" + cteBinary = "binary" + cteQuotedPrintable = "quoted-printable" + + // Standard MIME header names + hnContentDisposition = "Content-Disposition" + hnContentEncoding = "Content-Transfer-Encoding" + hnContentID = "Content-ID" + hnContentType = "Content-Type" + hnMIMEVersion = "MIME-Version" + + // Standard MIME header parameters + hpBoundary = "boundary" + hpCharset = "charset" + hpFile = "file" + hpFilename = "filename" + hpName = "name" + hpModDate = "modification-date" + + utf8 = "utf-8" +) + +// AddressHeaders is the set of SMTP headers that contain email addresses, used by +// Envelope.AddressList(). Key characters must be all lowercase. +var AddressHeaders = map[string]bool{ + "bcc": true, + "cc": true, + "delivered-to": true, + "from": true, + "reply-to": true, + "to": true, + "sender": true, + "resent-bcc": true, + "resent-cc": true, + "resent-from": true, + "resent-reply-to": true, + "resent-to": true, + "resent-sender": true, +} + +// ParseAddressList returns a mail.Address slice with RFC 2047 encoded names converted to UTF-8. +// It is more tolerant of malformed headers than the ParseAddressList func provided in Go's net/mail +// package. +func ParseAddressList(list string) ([]*mail.Address, error) { + parser := mail.AddressParser{WordDecoder: coding.NewExtMimeDecoder()} + + ret, err := parser.ParseList(list) + if err != nil { + switch err.Error() { + case "mail: expected comma": + // Attempt to add commas and parse again. + return parser.ParseList(stringutil.EnsureCommaDelimitedAddresses(list)) + case "mail: no address": + return nil, mail.ErrHeaderNotPresent + } + return nil, err + } + + for i := range ret { + // try to additionally decode with less strict decoder + ret[i].Name = coding.DecodeExtHeader(ret[i].Name) + ret[i].Address = coding.DecodeExtHeader(ret[i].Address) + } + + return ret, nil +} + +// ReadHeader reads a block of SMTP or MIME headers and returns a +// textproto.MIMEHeader. Header parse warnings & errors will be added to +// ErrorCollector, io errors will be returned directly. +func ReadHeader(r *bufio.Reader, p ErrorCollector) (textproto.MIMEHeader, error) { + // buf holds the massaged output for textproto.Reader.ReadMIMEHeader() + buf := &bytes.Buffer{} + tp := inttp.NewReader(r) + firstHeader := true +line: + for { + // Pull out each line of the headers as a temporary slice s + s, err := tp.ReadLineBytes() + if err != nil { + buf.Write([]byte{'\r', '\n'}) + break + } + + firstColon := bytes.IndexByte(s, ':') + firstSpace := bytes.IndexAny(s, " \t\n\r") + if firstSpace == 0 { + // Starts with space: continuation + buf.WriteByte(' ') + buf.Write(inttp.TrimBytes(s)) + continue + } + if firstColon == 0 { + // Can't parse line starting with colon: skip + p.AddError(ErrorMalformedHeader, "Header line %q started with a colon", s) + continue + } + if firstColon > 0 { + // Behavior change in net/textproto package in Golang 1.12.10 and 1.13.1: + // A space preceding the first colon in a header line is no longer handled + // automatically due to CVE-2019-16276 which takes advantage of this + // particular violation of RFC-7230 to exploit HTTP/1.1 + if bytes.Contains(s[:firstColon+1], []byte{' ', ':'}) { + s = bytes.Replace(s, []byte{' ', ':'}, []byte{':'}, 1) + firstColon = bytes.IndexByte(s, ':') + } + + // Behavior change in net/textproto package in Golang 1.20: invalid characters + // in header keys are no longer allowed; https://github.com/golang/go/issues/53188 + for _, c := range s[:firstColon] { + if c != ' ' && !inttp.ValidEmailHeaderFieldByte(c) { + p.AddError( + ErrorMalformedHeader, "Header name %q contains invalid character %q", s, c) + continue line + } + } + + // Contains a colon, treat as a new header line + if !firstHeader { + // New Header line, end the previous + buf.Write([]byte{'\r', '\n'}) + } + + s = inttp.TrimBytes(s) + buf.Write(s) + firstHeader = false + } else { + // No colon: potential non-indented continuation + if len(s) > 0 { + // Attempt to detect and repair a non-indented continuation of previous line + buf.WriteByte(' ') + buf.Write(s) + p.AddWarning(ErrorMalformedHeader, "Continued line %q was not indented", s) + } else { + // Empty line, finish header parsing + buf.Write([]byte{'\r', '\n'}) + break + } + } + } + + buf.Write([]byte{'\r', '\n'}) + tr := inttp.NewReader(bufio.NewReader(buf)) + header, err := tr.ReadEmailMIMEHeader() + return textproto.MIMEHeader(header), errors.WithStack(err) +} + +// readHeader reads a block of SMTP or MIME headers and returns a textproto.MIMEHeader. +// Header parse warnings & errors will be added to p.Errors, io errors will be returned directly. +func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) { + return ReadHeader(r, &partErrorCollector{p}) +} + +// decodeToUTF8Base64Header decodes a MIME header per RFC 2047, reencoding to =?utf-8b? +func decodeToUTF8Base64Header(input string) string { + if !strings.Contains(input, "=?") { + // Don't scan if there is nothing to do here + return input + } + + // The standard lib performs an incremental inspection of this string, where the + // "skipSpace" method only strings.trimLeft for spaces and tabs. Here we have a + // hard dependency on space existing and not on next expected rune. + // + // For resolving #112 with the least change, I will implement the + // "quoted display-name" detector, which will resolve the case specific + // issue stated in #112, but only in the case of a quoted display-name + // followed, without whitespace, by addr-spec. + tokens := strings.FieldsFunc(quotedDisplayName(input), whiteSpaceRune) + output := make([]string, len(tokens)) + + for i, token := range tokens { + if len(token) > 4 && strings.Contains(token, "=?") { + // Stash parenthesis, they should not be encoded + prefix := "" + suffix := "" + if token[0] == '(' { + prefix = "(" + token = token[1:] + } + if token[len(token)-1] == ')' { + suffix = ")" + token = token[:len(token)-1] + } + // Base64 encode token + output[i] = prefix + + mime.BEncoding.Encode("UTF-8", coding.DecodeExtHeader(token)) + + suffix + } else { + output[i] = token + } + } + + // Return space separated tokens + return strings.Join(output, " ") +} + +func quotedDisplayName(s string) string { + if !strings.HasPrefix(s, "\"") { + return s + } + idx := strings.LastIndex(s, "\"") + return fmt.Sprintf("%s %s", s[:idx+1], s[idx+1:]) +} + +// Detects a RFC-822 linear-white-space, passed to strings.FieldsFunc. +func whiteSpaceRune(r rune) bool { + return r == ' ' || r == '\t' || r == '\r' || r == '\n' +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/inspect.go b/vendor/github.com/jhillyerd/enmime/v2/inspect.go new file mode 100644 index 000000000..895421187 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/inspect.go @@ -0,0 +1,84 @@ +package enmime + +import ( + "bufio" + "bytes" + "io" + + "github.com/jhillyerd/enmime/v2/internal/coding" + "github.com/jhillyerd/enmime/v2/internal/textproto" + + "github.com/pkg/errors" +) + +var defaultHeadersList = []string{ + "From", + "To", + "Sender", + "CC", + "BCC", + "Subject", + "Date", +} + +// DecodeRFC2047 decodes the given string according to RFC 2047 and returns the +// decoded UTF-8 equivalent. If the input is not using RFC 2047 encoding, or the +// charset is not recognized, it will return the input unmodified. +func DecodeRFC2047(s string) string { + return coding.RFC2047Decode(s) +} + +// DecodeHeaders returns a limited selection of mime headers for use by user agents +// Default header list: +// +// "Date", "Subject", "Sender", "From", "To", "CC" and "BCC" +// +// Additional headers provided will be formatted canonically: +// +// h, err := enmime.DecodeHeaders(b, "content-type", "user-agent") +func DecodeHeaders(b []byte, addtlHeaders ...string) (textproto.MIMEHeader, error) { + b = ensureHeaderBoundary(b) + tr := textproto.NewReader(bufio.NewReader(bytes.NewReader(b))) + headers, err := tr.ReadMIMEHeader() + switch errors.Cause(err) { + case nil, io.EOF: + // carry on, io.EOF is expected + default: + return nil, err + } + headerList := defaultHeadersList + headerList = append(headerList, addtlHeaders...) + res := map[string][]string{} + for _, header := range headerList { + h := textproto.CanonicalEmailMIMEHeaderKey(header) + res[h] = make([]string, 0, len(headers[h])) + for _, value := range headers[h] { + res[h] = append(res[h], DecodeRFC2047(value)) + } + } + + return res, nil +} + +// ensureHeaderBoundary scans through an rfc822 document to ensure the boundary between headers and body exists +func ensureHeaderBoundary(b []byte) []byte { + slice := bytes.SplitAfter(b, []byte{'\r', '\n'}) + dest := make([]byte, 0, len(b)+2) + headers := true + for _, v := range slice { + if headers && (bytes.Contains(v, []byte{':'}) || bytes.HasPrefix(v, []byte{' '}) || bytes.HasPrefix(v, []byte{'\t'})) { + dest = append(dest, v...) + continue + } + if headers { + headers = false + if !bytes.Equal(v, []byte{'\r', '\n'}) { + dest = append(dest, append([]byte{'\r', '\n'}, v...)...) + continue + } + } + dest = append(dest, v...) + } + + return dest +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/coding/base64.go b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/base64.go new file mode 100644 index 000000000..10b829f77 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/base64.go @@ -0,0 +1,64 @@ +package coding + +import ( + "fmt" + "io" +) + +// base64CleanerTable notes byte values that should be stripped (-2), stripped w/ error (-1). +var base64CleanerTable = []int8{ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -1, -1, -2, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, +} + +// Base64Cleaner improves the tolerance of in Go's built-in base64 decoder by stripping out +// characters that would cause decoding to fail. +type Base64Cleaner struct { + // Report of non-whitespace characters detected while cleaning base64 data. + Errors []error + + r io.Reader + buffer [1024]byte +} + +// Enforce io.Reader interface. +var _ io.Reader = &Base64Cleaner{} + +// NewBase64Cleaner returns a Base64Cleaner object for the specified reader. Base64Cleaner +// implements the io.Reader interface. +func NewBase64Cleaner(r io.Reader) *Base64Cleaner { + return &Base64Cleaner{ + Errors: make([]error, 0), + r: r, + } +} + +// Read method for io.Reader interface. +func (bc *Base64Cleaner) Read(p []byte) (n int, err error) { + // Size our buf to smallest of len(p) or len(bc.buffer). + size := len(bc.buffer) + if size > len(p) { + size = len(p) + } + buf := bc.buffer[:size] + bn, err := bc.r.Read(buf) + for i := 0; i < bn; i++ { + switch base64CleanerTable[buf[i]&0x7f] { + case -2: + // Strip these silently: tab, \n, \r, space, equals sign. + case -1: + // Strip these, but warn the client. + bc.Errors = append(bc.Errors, fmt.Errorf("unexpected %q in base64 stream", buf[i])) + default: + p[n] = buf[i] + n++ + } + } + return +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/coding/charsets.go b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/charsets.go new file mode 100644 index 000000000..f93488f54 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/charsets.go @@ -0,0 +1,338 @@ +package coding + +import ( + "bytes" + "fmt" + "io" + "regexp" + "strings" + + "github.com/cention-sany/utf7" + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/encoding/japanese" + "golang.org/x/text/encoding/korean" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/encoding/traditionalchinese" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +const utf8 = "utf-8" + +// encodings is based on golang.org/x/net/html/charset/table.go +var encodings = map[string]struct { + e encoding.Encoding + name string +}{ + "unicode-1-1-utf-8": {encoding.Nop, utf8}, + "utf-8": {encoding.Nop, utf8}, + "utf8": {encoding.Nop, utf8}, + "utf-7": {utf7.UTF7, "utf-7"}, + "utf7": {utf7.UTF7, "utf-7"}, + "866": {charmap.CodePage866, "ibm866"}, + "cp866": {charmap.CodePage866, "ibm866"}, + "csibm866": {charmap.CodePage866, "ibm866"}, + "ibm866": {charmap.CodePage866, "ibm866"}, + "csisolatin2": {charmap.ISO8859_2, "iso-8859-2"}, + "iso-8859-2": {charmap.ISO8859_2, "iso-8859-2"}, + "iso-ir-101": {charmap.ISO8859_2, "iso-8859-2"}, + "iso8859-2": {charmap.ISO8859_2, "iso-8859-2"}, + "iso88592": {charmap.ISO8859_2, "iso-8859-2"}, + "iso_8859-2": {charmap.ISO8859_2, "iso-8859-2"}, + "iso_8859-2:1987": {charmap.ISO8859_2, "iso-8859-2"}, + "l2": {charmap.ISO8859_2, "iso-8859-2"}, + "latin2": {charmap.ISO8859_2, "iso-8859-2"}, + "csisolatin3": {charmap.ISO8859_3, "iso-8859-3"}, + "iso-8859-3": {charmap.ISO8859_3, "iso-8859-3"}, + "iso-ir-109": {charmap.ISO8859_3, "iso-8859-3"}, + "iso8859-3": {charmap.ISO8859_3, "iso-8859-3"}, + "iso88593": {charmap.ISO8859_3, "iso-8859-3"}, + "iso_8859-3": {charmap.ISO8859_3, "iso-8859-3"}, + "iso_8859-3:1988": {charmap.ISO8859_3, "iso-8859-3"}, + "l3": {charmap.ISO8859_3, "iso-8859-3"}, + "latin3": {charmap.ISO8859_3, "iso-8859-3"}, + "csisolatin4": {charmap.ISO8859_4, "iso-8859-4"}, + "iso-8859-4": {charmap.ISO8859_4, "iso-8859-4"}, + "iso-ir-110": {charmap.ISO8859_4, "iso-8859-4"}, + "iso8859-4": {charmap.ISO8859_4, "iso-8859-4"}, + "iso88594": {charmap.ISO8859_4, "iso-8859-4"}, + "iso_8859-4": {charmap.ISO8859_4, "iso-8859-4"}, + "iso_8859-4:1988": {charmap.ISO8859_4, "iso-8859-4"}, + "l4": {charmap.ISO8859_4, "iso-8859-4"}, + "latin4": {charmap.ISO8859_4, "iso-8859-4"}, + "csisolatincyrillic": {charmap.ISO8859_5, "iso-8859-5"}, + "cyrillic": {charmap.ISO8859_5, "iso-8859-5"}, + "iso-8859-5": {charmap.ISO8859_5, "iso-8859-5"}, + "iso-ir-144": {charmap.ISO8859_5, "iso-8859-5"}, + "iso8859-5": {charmap.ISO8859_5, "iso-8859-5"}, + "iso88595": {charmap.ISO8859_5, "iso-8859-5"}, + "iso_8859-5": {charmap.ISO8859_5, "iso-8859-5"}, + "iso_8859-5:1988": {charmap.ISO8859_5, "iso-8859-5"}, + "arabic": {charmap.ISO8859_6, "iso-8859-6"}, + "asmo-708": {charmap.ISO8859_6, "iso-8859-6"}, + "csiso88596e": {charmap.ISO8859_6, "iso-8859-6"}, + "csiso88596i": {charmap.ISO8859_6, "iso-8859-6"}, + "csisolatinarabic": {charmap.ISO8859_6, "iso-8859-6"}, + "ecma-114": {charmap.ISO8859_6, "iso-8859-6"}, + "iso-8859-6": {charmap.ISO8859_6, "iso-8859-6"}, + "iso-8859-6-e": {charmap.ISO8859_6, "iso-8859-6"}, + "iso-8859-6-i": {charmap.ISO8859_6, "iso-8859-6"}, + "iso-ir-127": {charmap.ISO8859_6, "iso-8859-6"}, + "iso8859-6": {charmap.ISO8859_6, "iso-8859-6"}, + "iso88596": {charmap.ISO8859_6, "iso-8859-6"}, + "iso_8859-6": {charmap.ISO8859_6, "iso-8859-6"}, + "iso_8859-6:1987": {charmap.ISO8859_6, "iso-8859-6"}, + "csisolatingreek": {charmap.ISO8859_7, "iso-8859-7"}, + "ecma-118": {charmap.ISO8859_7, "iso-8859-7"}, + "elot_928": {charmap.ISO8859_7, "iso-8859-7"}, + "greek": {charmap.ISO8859_7, "iso-8859-7"}, + "greek8": {charmap.ISO8859_7, "iso-8859-7"}, + "iso-8859-7": {charmap.ISO8859_7, "iso-8859-7"}, + "iso-ir-126": {charmap.ISO8859_7, "iso-8859-7"}, + "iso8859-7": {charmap.ISO8859_7, "iso-8859-7"}, + "iso88597": {charmap.ISO8859_7, "iso-8859-7"}, + "iso_8859-7": {charmap.ISO8859_7, "iso-8859-7"}, + "iso_8859-7:1987": {charmap.ISO8859_7, "iso-8859-7"}, + "sun_eu_greek": {charmap.ISO8859_7, "iso-8859-7"}, + "csiso88598e": {charmap.ISO8859_8, "iso-8859-8"}, + "csisolatinhebrew": {charmap.ISO8859_8, "iso-8859-8"}, + "hebrew": {charmap.ISO8859_8, "iso-8859-8"}, + "iso-8859-8": {charmap.ISO8859_8, "iso-8859-8"}, + "iso-8859-8-e": {charmap.ISO8859_8, "iso-8859-8"}, + "iso-ir-138": {charmap.ISO8859_8, "iso-8859-8"}, + "iso8859-8": {charmap.ISO8859_8, "iso-8859-8"}, + "iso88598": {charmap.ISO8859_8, "iso-8859-8"}, + "iso_8859-8": {charmap.ISO8859_8, "iso-8859-8"}, + "iso_8859-8:1988": {charmap.ISO8859_8, "iso-8859-8"}, + "visual": {charmap.ISO8859_8, "iso-8859-8"}, + "csiso88598i": {charmap.ISO8859_8, "iso-8859-8-i"}, + "iso-8859-8-i": {charmap.ISO8859_8, "iso-8859-8-i"}, + "logical": {charmap.ISO8859_8, "iso-8859-8-i"}, + "csisolatin6": {charmap.ISO8859_10, "iso-8859-10"}, + "iso-8859-10": {charmap.ISO8859_10, "iso-8859-10"}, + "iso-ir-157": {charmap.ISO8859_10, "iso-8859-10"}, + "iso8859-10": {charmap.ISO8859_10, "iso-8859-10"}, + "iso885910": {charmap.ISO8859_10, "iso-8859-10"}, + "l6": {charmap.ISO8859_10, "iso-8859-10"}, + "latin6": {charmap.ISO8859_10, "iso-8859-10"}, + "iso-8859-13": {charmap.ISO8859_13, "iso-8859-13"}, + "iso8859-13": {charmap.ISO8859_13, "iso-8859-13"}, + "iso885913": {charmap.ISO8859_13, "iso-8859-13"}, + "iso-8859-14": {charmap.ISO8859_14, "iso-8859-14"}, + "iso8859-14": {charmap.ISO8859_14, "iso-8859-14"}, + "iso885914": {charmap.ISO8859_14, "iso-8859-14"}, + "csisolatin9": {charmap.ISO8859_15, "iso-8859-15"}, + "iso-8859-15": {charmap.ISO8859_15, "iso-8859-15"}, + "iso8859-15": {charmap.ISO8859_15, "iso-8859-15"}, + "iso885915": {charmap.ISO8859_15, "iso-8859-15"}, + "iso_8859-15": {charmap.ISO8859_15, "iso-8859-15"}, + "l9": {charmap.ISO8859_15, "iso-8859-15"}, + "iso-8859-16": {charmap.ISO8859_16, "iso-8859-16"}, + "cskoi8r": {charmap.KOI8R, "koi8-r"}, + "koi": {charmap.KOI8R, "koi8-r"}, + "koi8": {charmap.KOI8R, "koi8-r"}, + "koi8-r": {charmap.KOI8R, "koi8-r"}, + "koi8_r": {charmap.KOI8R, "koi8-r"}, + "koi8-u": {charmap.KOI8U, "koi8-u"}, + "csmacintosh": {charmap.Macintosh, "macintosh"}, + "mac": {charmap.Macintosh, "macintosh"}, + "macintosh": {charmap.Macintosh, "macintosh"}, + "x-mac-roman": {charmap.Macintosh, "macintosh"}, + "dos-874": {charmap.Windows874, "windows-874"}, + "iso-8859-11": {charmap.Windows874, "windows-874"}, + "iso8859-11": {charmap.Windows874, "windows-874"}, + "iso885911": {charmap.Windows874, "windows-874"}, + "tis-620": {charmap.Windows874, "windows-874"}, + "windows-874": {charmap.Windows874, "windows-874"}, + "cp1250": {charmap.Windows1250, "windows-1250"}, + "windows-1250": {charmap.Windows1250, "windows-1250"}, + "x-cp1250": {charmap.Windows1250, "windows-1250"}, + "cp1251": {charmap.Windows1251, "windows-1251"}, + "windows-1251": {charmap.Windows1251, "windows-1251"}, + "x-cp1251": {charmap.Windows1251, "windows-1251"}, + "ansi_x3.4-1968": {charmap.Windows1252, "windows-1252"}, + "ascii": {charmap.Windows1252, "windows-1252"}, + "cp1252": {charmap.Windows1252, "windows-1252"}, + "cp819": {charmap.Windows1252, "windows-1252"}, + "csisolatin1": {charmap.Windows1252, "windows-1252"}, + "ibm819": {charmap.Windows1252, "windows-1252"}, + "iso-8859-1": {charmap.ISO8859_1, "iso-8859-1"}, + "iso-ir-100": {charmap.Windows1252, "windows-1252"}, + "iso8859-1": {charmap.ISO8859_1, "iso-8859-1"}, + "iso8859_1": {charmap.ISO8859_1, "iso-8859-1"}, + "iso88591": {charmap.ISO8859_1, "iso-8859-1"}, + "iso_8859-1": {charmap.ISO8859_1, "iso-8859-1"}, + "iso_8859-1:1987": {charmap.ISO8859_1, "iso-8859-1"}, + "l1": {charmap.Windows1252, "windows-1252"}, + "latin1": {charmap.Windows1252, "windows-1252"}, + "us-ascii": {charmap.Windows1252, "windows-1252"}, + "windows-1252": {charmap.Windows1252, "windows-1252"}, + "x-cp1252": {charmap.Windows1252, "windows-1252"}, + "cp1253": {charmap.Windows1253, "windows-1253"}, + "windows-1253": {charmap.Windows1253, "windows-1253"}, + "x-cp1253": {charmap.Windows1253, "windows-1253"}, + "cp1254": {charmap.Windows1254, "windows-1254"}, + "csisolatin5": {charmap.Windows1254, "windows-1254"}, + "iso-8859-9": {charmap.Windows1254, "windows-1254"}, + "iso-ir-148": {charmap.Windows1254, "windows-1254"}, + "iso8859-9": {charmap.Windows1254, "windows-1254"}, + "iso88599": {charmap.Windows1254, "windows-1254"}, + "iso_8859-9": {charmap.Windows1254, "windows-1254"}, + "iso_8859-9:1989": {charmap.Windows1254, "windows-1254"}, + "l5": {charmap.Windows1254, "windows-1254"}, + "latin5": {charmap.Windows1254, "windows-1254"}, + "windows-1254": {charmap.Windows1254, "windows-1254"}, + "x-cp1254": {charmap.Windows1254, "windows-1254"}, + "cp1255": {charmap.Windows1255, "windows-1255"}, + "windows-1255": {charmap.Windows1255, "windows-1255"}, + "x-cp1255": {charmap.Windows1255, "windows-1255"}, + "cp1256": {charmap.Windows1256, "windows-1256"}, + "windows-1256": {charmap.Windows1256, "windows-1256"}, + "x-cp1256": {charmap.Windows1256, "windows-1256"}, + "cp1257": {charmap.Windows1257, "windows-1257"}, + "windows-1257": {charmap.Windows1257, "windows-1257"}, + "x-cp1257": {charmap.Windows1257, "windows-1257"}, + "cp1258": {charmap.Windows1258, "windows-1258"}, + "windows-1258": {charmap.Windows1258, "windows-1258"}, + "x-cp1258": {charmap.Windows1258, "windows-1258"}, + "x-mac-cyrillic": {charmap.MacintoshCyrillic, "x-mac-cyrillic"}, + "x-mac-ukrainian": {charmap.MacintoshCyrillic, "x-mac-cyrillic"}, + "chinese": {simplifiedchinese.GBK, "gbk"}, + "csgb2312": {simplifiedchinese.GBK, "gbk"}, + "csiso58gb231280": {simplifiedchinese.GBK, "gbk"}, + "gb2312": {simplifiedchinese.GBK, "gbk"}, + "gb_2312": {simplifiedchinese.GBK, "gbk"}, + "gb_2312-80": {simplifiedchinese.GBK, "gbk"}, + "gbk": {simplifiedchinese.GBK, "gbk"}, + "iso-ir-58": {simplifiedchinese.GBK, "gbk"}, + "x-gbk": {simplifiedchinese.GBK, "gbk"}, + "gb18030": {simplifiedchinese.GB18030, "gb18030"}, + "hz-gb-2312": {simplifiedchinese.HZGB2312, "hz-gb-2312"}, + "big5": {traditionalchinese.Big5, "big5"}, + "big5-hkscs": {traditionalchinese.Big5, "big5"}, + "cn-big5": {traditionalchinese.Big5, "big5"}, + "csbig5": {traditionalchinese.Big5, "big5"}, + "x-x-big5": {traditionalchinese.Big5, "big5"}, + "cseucpkdfmtjapanese": {japanese.EUCJP, "euc-jp"}, + "euc-jp": {japanese.EUCJP, "euc-jp"}, + "x-euc-jp": {japanese.EUCJP, "euc-jp"}, + "csiso2022jp": {japanese.ISO2022JP, "iso-2022-jp"}, + "iso-2022-jp": {japanese.ISO2022JP, "iso-2022-jp"}, + "csshiftjis": {japanese.ShiftJIS, "shift_jis"}, + "ms_kanji": {japanese.ShiftJIS, "shift_jis"}, + "shift-jis": {japanese.ShiftJIS, "shift_jis"}, + "shift_jis": {japanese.ShiftJIS, "shift_jis"}, + "sjis": {japanese.ShiftJIS, "shift_jis"}, + "windows-31j": {japanese.ShiftJIS, "shift_jis"}, + "x-sjis": {japanese.ShiftJIS, "shift_jis"}, + "cseuckr": {korean.EUCKR, "euc-kr"}, + "csksc56011987": {korean.EUCKR, "euc-kr"}, + "euc-kr": {korean.EUCKR, "euc-kr"}, + "iso-ir-149": {korean.EUCKR, "euc-kr"}, + "korean": {korean.EUCKR, "euc-kr"}, + "ks_c_5601-1987": {korean.EUCKR, "euc-kr"}, + "ks_c_5601-1989": {korean.EUCKR, "euc-kr"}, + "ksc5601": {korean.EUCKR, "euc-kr"}, + "ksc_5601": {korean.EUCKR, "euc-kr"}, + "windows-949": {korean.EUCKR, "euc-kr"}, + "csiso2022kr": {encoding.Replacement, "replacement"}, + "iso-2022-kr": {encoding.Replacement, "replacement"}, + "iso-2022-cn": {encoding.Replacement, "replacement"}, + "iso-2022-cn-ext": {encoding.Replacement, "replacement"}, + "utf-16be": {unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), "utf-16be"}, + "utf-16": {unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), "utf-16le"}, + "utf-16le": {unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), "utf-16le"}, + "x-user-defined": {charmap.XUserDefined, "x-user-defined"}, + "iso646-us": {charmap.Windows1252, "windows-1252"}, // ISO646 isn't us-ascii but 1991 version is. + "iso: western": {charmap.Windows1252, "windows-1252"}, // same as iso-8859-1 + "we8iso8859p1": {charmap.Windows1252, "windows-1252"}, // same as iso-8859-1 + "cp936": {simplifiedchinese.GBK, "gbk"}, // same as gb2312 + "cp850": {charmap.CodePage850, "cp850"}, + "cp-850": {charmap.CodePage850, "cp850"}, + "ibm850": {charmap.CodePage850, "cp850"}, + "136": {traditionalchinese.Big5, "big5"}, // same as chinese big5 + "cp932": {japanese.ShiftJIS, "shift_jis"}, + "8859-1": {charmap.Windows1252, "windows-1252"}, + "8859_1": {charmap.Windows1252, "windows-1252"}, + "8859-2": {charmap.ISO8859_2, "iso-8859-2"}, + "8859_2": {charmap.ISO8859_2, "iso-8859-2"}, + "8859-3": {charmap.ISO8859_3, "iso-8859-3"}, + "8859_3": {charmap.ISO8859_3, "iso-8859-3"}, + "8859-4": {charmap.ISO8859_4, "iso-8859-4"}, + "8859_4": {charmap.ISO8859_4, "iso-8859-4"}, + "8859-5": {charmap.ISO8859_5, "iso-8859-5"}, + "8859_5": {charmap.ISO8859_5, "iso-8859-5"}, + "8859-6": {charmap.ISO8859_6, "iso-8859-6"}, + "8859_6": {charmap.ISO8859_6, "iso-8859-6"}, + "8859-7": {charmap.ISO8859_7, "iso-8859-7"}, + "8859_7": {charmap.ISO8859_7, "iso-8859-7"}, + "8859-8": {charmap.ISO8859_8, "iso-8859-8"}, + "8859_8": {charmap.ISO8859_8, "iso-8859-8"}, + "8859-10": {charmap.ISO8859_10, "iso-8859-10"}, + "8859_10": {charmap.ISO8859_10, "iso-8859-10"}, + "8859-13": {charmap.ISO8859_13, "iso-8859-13"}, + "8859_13": {charmap.ISO8859_13, "iso-8859-13"}, + "8859-14": {charmap.ISO8859_14, "iso-8859-14"}, + "8859_14": {charmap.ISO8859_14, "iso-8859-14"}, + "8859-15": {charmap.ISO8859_15, "iso-8859-15"}, + "8859_15": {charmap.ISO8859_15, "iso-8859-15"}, + "8859-16": {charmap.ISO8859_16, "iso-8859-16"}, + "8859_16": {charmap.ISO8859_16, "iso-8859-16"}, + "utf8mb4": {encoding.Nop, "utf-8"}, // emojis, but golang can handle it directly + "238": {charmap.Windows1250, "windows-1250"}, +} + +var metaTagCharsetRegexp = regexp.MustCompile( + `(?i)[a-zA-Z0-9_.:-]+)\s*"?`) +var metaTagCharsetIndex int + +func init() { + // Find the submatch index for charset in metaTagCharsetRegexp + for i, name := range metaTagCharsetRegexp.SubexpNames() { + if name == "charset" { + metaTagCharsetIndex = i + break + } + } +} + +// ConvertToUTF8String uses the provided charset to decode a slice of bytes into a normal +// UTF-8 string. +func ConvertToUTF8String(charset string, textBytes []byte) (string, error) { + csentry, ok := encodings[strings.ToLower(charset)] + if !ok { + return "", fmt.Errorf("unsupported charset %q", charset) + } + input := bytes.NewReader(textBytes) + reader := transform.NewReader(input, csentry.e.NewDecoder()) + output, err := io.ReadAll(reader) + if err != nil { + return "", err + } + return string(output), nil +} + +// NewCharsetReader generates charset-conversion readers, converting from the provided charset into +// UTF-8. CharsetReader is a factory signature defined by Go's mime.WordDecoder. +// +// This function is similar to: https://godoc.org/golang.org/x/net/html/charset#NewReaderLabel +func NewCharsetReader(charset string, input io.Reader) (io.Reader, error) { + if strings.ToLower(charset) == utf8 { + return input, nil + } + csentry, ok := encodings[strings.ToLower(charset)] + if !ok { + return nil, fmt.Errorf("unsupported charset %q", charset) + } + return transform.NewReader(input, csentry.e.NewDecoder()), nil +} + +// FindCharsetInHTML looks for charset in the HTML meta tag (v4.01 and v5). +func FindCharsetInHTML(html string) string { + charsetMatches := metaTagCharsetRegexp.FindAllStringSubmatch(html, -1) + if len(charsetMatches) > 0 { + return charsetMatches[0][metaTagCharsetIndex] + } + return "" +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/coding/headerext.go b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/headerext.go new file mode 100644 index 000000000..6aec17752 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/headerext.go @@ -0,0 +1,135 @@ +package coding + +import ( + "fmt" + "io" + "mime" + "strings" +) + +// NewExtMimeDecoder creates new MIME word decoder which allows decoding of additional charsets. +func NewExtMimeDecoder() *mime.WordDecoder { + return &mime.WordDecoder{ + CharsetReader: NewCharsetReader, + } +} + +// DecodeExtHeader decodes a single line (per RFC 2047, aka Message Header Extensions) using Golang's +// mime.WordDecoder. +func DecodeExtHeader(input string) string { + if !strings.Contains(input, "=?") { + // Don't scan if there is nothing to do here + return input + } + + header, err := NewExtMimeDecoder().DecodeHeader(input) + if err != nil { + return input + } + + return header +} + +// RFC2047Decode returns a decoded string if the input uses RFC2047 encoding, otherwise it will +// return the input. +// +// RFC2047 Example: `=?UTF-8?B?bmFtZT0iw7DCn8KUwoo=?=` +func RFC2047Decode(s string) string { + // Convert CR/LF to spaces. + s = strings.Map(func(r rune) rune { + if r == '\n' || r == '\r' { + return ' ' + } + return r + }, s) + + var err error + decoded := false + for { + s, err = rfc2047Recurse(s) + switch err { + case nil: + decoded = true + continue + + default: + if decoded { + key, value, found := strings.Cut(s, "=") + if !found { + return s + } + + // Add quotes as needed. + if !strings.HasPrefix(value, "\"") { + value = `"` + value + } + if !strings.HasSuffix(value, "\"") { + value += `"` + } + + return fmt.Sprintf("%s=%s", key, value) + } + + return s + } + } +} + +// rfc2047Recurse is called for if the value contains content encoded in RFC2047 format and decodes +// it. +func rfc2047Recurse(s string) (string, error) { + us := strings.ToUpper(s) + if !strings.Contains(us, "?Q?") && !strings.Contains(us, "?B?") { + return s, io.EOF + } + + var val string + if val = DecodeExtHeader(s); val == s { + if val = DecodeExtHeader(fixRFC2047String(val)); val == s { + return val, io.EOF + } + } + + return val, nil +} + +// fixRFC2047String removes the following characters from charset and encoding segments of an +// RFC2047 string: '\n', '\r' and ' ' +func fixRFC2047String(s string) string { + inString := false + isWithinTerminatingEqualSigns := false + questionMarkCount := 0 + sb := &strings.Builder{} + for _, v := range s { + switch v { + case '=': + if questionMarkCount == 3 { + inString = false + } else { + isWithinTerminatingEqualSigns = true + } + sb.WriteRune(v) + + case '?': + if isWithinTerminatingEqualSigns { + inString = true + } else { + questionMarkCount++ + } + isWithinTerminatingEqualSigns = false + sb.WriteRune(v) + + case '\n', '\r', ' ': + if !inString { + sb.WriteRune(v) + } + isWithinTerminatingEqualSigns = false + + default: + isWithinTerminatingEqualSigns = false + sb.WriteRune(v) + } + } + + return sb.String() +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/coding/idheader.go b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/idheader.go new file mode 100644 index 000000000..9003a8876 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/idheader.go @@ -0,0 +1,26 @@ +package coding + +import ( + "net/url" + "strings" +) + +// FromIDHeader decodes a Content-ID or Message-ID header value (RFC 2392) into a utf-8 string. +// Example: "" becomes "foo?bar baz". +func FromIDHeader(v string) string { + if v == "" { + return v + } + v = strings.TrimLeft(v, "<") + v = strings.TrimRight(v, ">") + if r, err := url.QueryUnescape(v); err == nil { + v = r + } + return v +} + +// ToIDHeader encodes a Content-ID or Message-ID header value (RFC 2392) from a utf-8 string. +func ToIDHeader(v string) string { + v = url.QueryEscape(v) + return "<" + strings.ReplaceAll(v, "%40", "@") + ">" +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/coding/quotedprint.go b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/quotedprint.go new file mode 100644 index 000000000..6ebeb7d32 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/coding/quotedprint.go @@ -0,0 +1,161 @@ +package coding + +import ( + "bufio" + "fmt" + "io" +) + +// QPCleaner scans quoted printable content for invalid characters and encodes them so that +// Go's quoted-printable decoder does not abort with an error. +type QPCleaner struct { + in *bufio.Reader + overflow []byte + lineLen int +} + +// MaxQPLineLen is the maximum line length we allow before inserting `=\r\n`. Prevents buffer +// overflows in mime/quotedprintable.Reader. +const MaxQPLineLen = 1024 + +var ( + _ io.Reader = &QPCleaner{} // Assert QPCleaner implements io.Reader. + + escapedEquals = []byte("=3D") // QP encoded value of an equals sign. + lineBreak = []byte("=\r\n") +) + +// NewQPCleaner returns a QPCleaner for the specified reader. +func NewQPCleaner(r io.Reader) *QPCleaner { + return &QPCleaner{ + in: bufio.NewReader(r), + overflow: nil, + lineLen: 0, + } +} + +// Read method for io.Reader interface. +func (qp *QPCleaner) Read(dest []byte) (n int, err error) { + destLen := len(dest) + + if len(qp.overflow) > 0 { + // Copy bytes that didn't fit into dest buffer during previous read. + n = copy(dest, qp.overflow) + qp.overflow = qp.overflow[n:] + } + + // writeByte outputs a single byte, space for which will have already been ensured by the loop + // condition. Updates counters. + writeByte := func(in byte) { + dest[n] = in + n++ + qp.lineLen++ + } + + // safeWriteByte outputs a single byte, storing overflow for next read. Updates counters. + safeWriteByte := func(in byte) { + if n < destLen { + dest[n] = in + n++ + } else { + qp.overflow = append(qp.overflow, in) + } + qp.lineLen++ + } + + // writeBytes outputs multiple bytes, storing overflow for next read. Updates counters. + writeBytes := func(in []byte) { + nc := copy(dest[n:], in) + if nc < len(in) { + // Stash unwritten bytes into overflow. + qp.overflow = append(qp.overflow, []byte(in[nc:])...) + } + n += nc + qp.lineLen += len(in) + } + + // ensureLineLen ensures there is room to write `requested` bytes, preventing a line break being + // inserted in the middle of the escaped string. The requested count is in addition to the + // byte that was already reserved for this loop iteration. + ensureLineLen := func(requested int) { + if qp.lineLen+requested >= MaxQPLineLen { + writeBytes(lineBreak) + qp.lineLen = 0 + } + } + + // Loop over bytes in qp.in ByteReader while there is space in dest. + for n < destLen { + var b byte + b, err = qp.in.ReadByte() + if err != nil { + return n, err + } + + if qp.lineLen >= MaxQPLineLen { + writeBytes(lineBreak) + qp.lineLen = 0 + if n == destLen { + break + } + } + + switch { + // Pass valid hex bytes through, otherwise escapes the equals symbol. + case b == '=': + ensureLineLen(2) + + var hexBytes []byte + hexBytes, err = qp.in.Peek(2) + if err != nil && err != io.EOF { + return 0, err + } + if validHexBytes(hexBytes) { + safeWriteByte(b) + } else { + writeBytes(escapedEquals) + } + + // Valid special character. + case b == '\t': + writeByte(b) + + // Valid special characters that reset line length. + case b == '\r' || b == '\n': + writeByte(b) + qp.lineLen = 0 + + // Invalid characters, render as quoted-printable. + case b < ' ' || '~' < b: + ensureLineLen(2) + writeBytes([]byte(fmt.Sprintf("=%02X", b))) + + // Acceptable characters. + default: + writeByte(b) + } + } + + return n, err +} + +func validHexByte(b byte) bool { + return '0' <= b && b <= '9' || 'A' <= b && b <= 'F' || 'a' <= b && b <= 'f' +} + +// validHexBytes returns true if this byte sequence represents a valid quoted-printable escape +// sequence or line break, minus the initial equals sign. +func validHexBytes(v []byte) bool { + if len(v) > 0 && v[0] == '\n' { + // Soft line break. + return true + } + if len(v) < 2 { + return false + } + if v[0] == '\r' && v[1] == '\n' { + // Soft line break. + return true + } + return validHexByte(v[0]) && validHexByte(v[1]) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/addr.go b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/addr.go new file mode 100644 index 000000000..4a02a83ad --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/addr.go @@ -0,0 +1,106 @@ +package stringutil + +import ( + "bytes" + "net/mail" + "strings" +) + +// JoinAddress formats a slice of Address structs such that they can be used in a To or Cc header. +func JoinAddress(addrs []mail.Address) string { + if len(addrs) == 0 { + return "" + } + buf := &bytes.Buffer{} + for i, a := range addrs { + if i > 0 { + _, _ = buf.WriteString(", ") + } + _, _ = buf.WriteString(a.String()) + } + return buf.String() +} + +// EnsureCommaDelimitedAddresses is used by AddressList to ensure that address lists are properly +// delimited. +func EnsureCommaDelimitedAddresses(s string) string { + // This normalizes the whitespace, but may interfere with CFWS (comments with folding whitespace) + // RFC-5322 3.4.0: + // because some legacy implementations interpret the comment, + // comments generally SHOULD NOT be used in address fields + // to avoid confusing such implementations. + s = strings.Join(strings.Fields(s), " ") + + inQuotes := false + inDomain := false + escapeSequence := false + inAngles := false + sb := strings.Builder{} + for i, r := range s { + if escapeSequence { + escapeSequence = false + sb.WriteRune(r) + continue + } + if r == '"' { + inQuotes = !inQuotes + sb.WriteRune(r) + continue + } + + if r == '<' { + inAngles = true + sb.WriteRune(r) + continue + } + + if r == '>' { + inAngles = false + sb.WriteRune(r) + continue + } + + if inQuotes { + if r == '\\' { + escapeSequence = true + sb.WriteRune(r) + continue + } + } else { + if r == '@' { + inDomain = true + sb.WriteRune(r) + continue + } + if inDomain { + if r == ';' { + inDomain = false + if i == len(s)-1 { + // omit trailing semicolon + continue + } + + sb.WriteRune(',') + continue + } + if r == ',' { + inDomain = false + sb.WriteRune(r) + continue + } + if r == ' ' && !inAngles { + inDomain = false + sb.WriteRune(',') + sb.WriteRune(r) + continue + } + } + + if inAngles && r == ' ' { + continue + } + } + sb.WriteRune(r) + } + return sb.String() +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/find.go b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/find.go new file mode 100644 index 000000000..2ef86fa32 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/find.go @@ -0,0 +1,38 @@ +package stringutil + +// FindUnquoted returns the indexes of the instance of v in s, or empty slice if v is not present in s. +// It ignores v present inside quoted runs. +func FindUnquoted(s string, v rune, quote rune) []int { + escaped := false + quoted := false + indexes := make([]int, 0) + quotedIndexes := make([]int, 0) + + for i := 0; i < len(s); i++ { + switch rune(s[i]) { + case escape: + escaped = !escaped // escape can escape itself. + case quote: + if escaped { + escaped = false + continue + } + + quoted = !quoted + if !quoted { + quotedIndexes = quotedIndexes[:0] // drop possible indices inside quoted segment + } + case v: + escaped = false + if quoted { + quotedIndexes = append(quotedIndexes, i) + } else { + indexes = append(indexes, i) + } + default: + escaped = false + } + } + + return append(indexes, quotedIndexes...) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/rand_source.go b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/rand_source.go new file mode 100644 index 000000000..30798061a --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/rand_source.go @@ -0,0 +1,43 @@ +package stringutil + +import ( + "math/rand" + "sync" + "time" +) + +var globalRandSource rand.Source + +func init() { + globalRandSource = NewLockedSource(time.Now().UTC().UnixNano()) +} + +// NewLockedSource creates a source of randomness using the given seed. +func NewLockedSource(seed int64) rand.Source64 { + return &lockedSource{ + s: rand.NewSource(seed).(rand.Source64), + } +} + +type lockedSource struct { + lock sync.Mutex + s rand.Source64 +} + +func (x *lockedSource) Int63() int64 { + x.lock.Lock() + defer x.lock.Unlock() + return x.s.Int63() +} + +func (x *lockedSource) Uint64() uint64 { + x.lock.Lock() + defer x.lock.Unlock() + return x.s.Uint64() +} + +func (x *lockedSource) Seed(seed int64) { + x.lock.Lock() + defer x.lock.Unlock() + x.s.Seed(seed) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/split.go b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/split.go new file mode 100644 index 000000000..04051be10 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/split.go @@ -0,0 +1,45 @@ +package stringutil + +const escape = '\\' + +// SplitUnquoted slices s into all substrings separated by sep and returns a slice of +// the substrings between those separators. +// +// If s does not contain sep and sep is not empty, SplitUnquoted returns a +// slice of length 1 whose only element is s. +// +// It ignores sep present inside quoted runs. +func SplitUnquoted(s string, sep rune, quote rune) []string { + return splitUnquoted(s, sep, quote, false) +} + +// SplitAfterUnquoted slices s into all substrings after each instance of sep and +// returns a slice of those substrings. +// +// If s does not contain sep and sep is not empty, SplitAfterUnquoted returns +// a slice of length 1 whose only element is s. +// +// It ignores sep present inside quoted runs. +func SplitAfterUnquoted(s string, sep rune, quote rune) []string { + return splitUnquoted(s, sep, quote, true) +} + +func splitUnquoted(s string, sep rune, quote rune, preserveSep bool) []string { + ixs := FindUnquoted(s, sep, quote) + if len(ixs) == 0 { + return []string{s} + } + + start := 0 + result := make([]string, 0, len(ixs)+1) + for _, ix := range ixs { + end := ix + if preserveSep { + end++ + } + result = append(result, s[start:end]) + start = ix + 1 + } + + return append(result, s[start:]) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/uuid.go b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/uuid.go new file mode 100644 index 000000000..167915f86 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/uuid.go @@ -0,0 +1,20 @@ +package stringutil + +import ( + "fmt" + "math/rand" +) + +// UUID generates a random UUID according to RFC 4122, using optional rand if supplied +func UUID(rs rand.Source) string { + uuid := make([]byte, 16) + if rs == nil { + rs = globalRandSource + } + _, _ = rand.New(rs).Read(uuid) + // variant bits; see section 4.1.1 + uuid[8] = uuid[8]&^0xc0 | 0x80 + // version 4 (pseudo-random); see section 4.1.3 + uuid[6] = uuid[6]&^0xf0 | 0x40 + return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/wrap.go b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/wrap.go new file mode 100644 index 000000000..becf9bc61 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/stringutil/wrap.go @@ -0,0 +1,36 @@ +package stringutil + +// Wrap builds a byte slice from strs, wrapping on word boundaries before maxLen chars +func Wrap(maxLen int, strs ...string) []byte { + input := make([]byte, 0) + output := make([]byte, 0) + for _, s := range strs { + input = append(input, []byte(s)...) + } + if len(input) < maxLen { + // Doesn't need to be wrapped + return input + } + ls := -1 // Last seen space index + lw := -1 // Last written byte index + ll := 0 // Length of current line + for i := 0; i < len(input); i++ { + ll++ + switch input[i] { + case ' ', '\t': + ls = i + } + if ll >= maxLen { + if ls >= 0 { + output = append(output, input[lw+1:ls]...) + output = append(output, '\r', '\n', ' ') + lw = ls // Jump over the space we broke on + ll = 1 // Count leading space above + // Rewind + i = lw + 1 + ls = -1 + } + } + } + return append(output, input[lw+1:]...) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/header.go b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/header.go new file mode 100644 index 000000000..b5103bbf9 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/header.go @@ -0,0 +1,56 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package textproto + +// A MIMEHeader represents a MIME-style header mapping +// keys to sets of values. +type MIMEHeader map[string][]string + +// Add adds the key, value pair to the header. +// It appends to any existing values associated with key. +func (h MIMEHeader) Add(key, value string) { + key = CanonicalEmailMIMEHeaderKey(key) + h[key] = append(h[key], value) +} + +// Set sets the header entries associated with key to +// the single element value. It replaces any existing +// values associated with key. +func (h MIMEHeader) Set(key, value string) { + h[CanonicalEmailMIMEHeaderKey(key)] = []string{value} +} + +// Get gets the first value associated with the given key. +// It is case insensitive; CanonicalMIMEHeaderKey is used +// to canonicalize the provided key. +// If there are no values associated with the key, Get returns "". +// To use non-canonical keys, access the map directly. +func (h MIMEHeader) Get(key string) string { + if h == nil { + return "" + } + v := h[CanonicalEmailMIMEHeaderKey(key)] + if len(v) == 0 { + return "" + } + return v[0] +} + +// Values returns all values associated with the given key. +// It is case insensitive; CanonicalMIMEHeaderKey is +// used to canonicalize the provided key. To use non-canonical +// keys, access the map directly. +// The returned slice is not a copy. +func (h MIMEHeader) Values(key string) []string { + if h == nil { + return nil + } + return h[CanonicalEmailMIMEHeaderKey(key)] +} + +// Del deletes the values associated with key. +func (h MIMEHeader) Del(key string) { + delete(h, CanonicalEmailMIMEHeaderKey(key)) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/pipeline.go b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/pipeline.go new file mode 100644 index 000000000..1928a306b --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/pipeline.go @@ -0,0 +1,118 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package textproto + +import ( + "sync" +) + +// A Pipeline manages a pipelined in-order request/response sequence. +// +// To use a Pipeline p to manage multiple clients on a connection, +// each client should run: +// +// id := p.Next() // take a number +// +// p.StartRequest(id) // wait for turn to send request +// «send request» +// p.EndRequest(id) // notify Pipeline that request is sent +// +// p.StartResponse(id) // wait for turn to read response +// «read response» +// p.EndResponse(id) // notify Pipeline that response is read +// +// A pipelined server can use the same calls to ensure that +// responses computed in parallel are written in the correct order. +type Pipeline struct { + mu sync.Mutex + id uint + request sequencer + response sequencer +} + +// Next returns the next id for a request/response pair. +func (p *Pipeline) Next() uint { + p.mu.Lock() + id := p.id + p.id++ + p.mu.Unlock() + return id +} + +// StartRequest blocks until it is time to send (or, if this is a server, receive) +// the request with the given id. +func (p *Pipeline) StartRequest(id uint) { + p.request.Start(id) +} + +// EndRequest notifies p that the request with the given id has been sent +// (or, if this is a server, received). +func (p *Pipeline) EndRequest(id uint) { + p.request.End(id) +} + +// StartResponse blocks until it is time to receive (or, if this is a server, send) +// the request with the given id. +func (p *Pipeline) StartResponse(id uint) { + p.response.Start(id) +} + +// EndResponse notifies p that the response with the given id has been received +// (or, if this is a server, sent). +func (p *Pipeline) EndResponse(id uint) { + p.response.End(id) +} + +// A sequencer schedules a sequence of numbered events that must +// happen in order, one after the other. The event numbering must start +// at 0 and increment without skipping. The event number wraps around +// safely as long as there are not 2^32 simultaneous events pending. +type sequencer struct { + mu sync.Mutex + id uint + wait map[uint]chan struct{} +} + +// Start waits until it is time for the event numbered id to begin. +// That is, except for the first event, it waits until End(id-1) has +// been called. +func (s *sequencer) Start(id uint) { + s.mu.Lock() + if s.id == id { + s.mu.Unlock() + return + } + c := make(chan struct{}) + if s.wait == nil { + s.wait = make(map[uint]chan struct{}) + } + s.wait[id] = c + s.mu.Unlock() + <-c +} + +// End notifies the sequencer that the event numbered id has completed, +// allowing it to schedule the event numbered id+1. It is a run-time error +// to call End with an id that is not the number of the active event. +func (s *sequencer) End(id uint) { + s.mu.Lock() + if s.id != id { + s.mu.Unlock() + panic("out of sync") + } + id++ + s.id = id + if s.wait == nil { + s.wait = make(map[uint]chan struct{}) + } + c, ok := s.wait[id] + if ok { + delete(s.wait, id) + } + s.mu.Unlock() + if ok { + close(c) + } +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/reader.go b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/reader.go new file mode 100644 index 000000000..73759f9b0 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/reader.go @@ -0,0 +1,802 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package textproto + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "math" + "net/textproto" + "strconv" + "strings" + "sync" +) + +// A Reader implements convenience methods for reading requests +// or responses from a text protocol network connection. +type Reader struct { + R *bufio.Reader + dot *dotReader + buf []byte // a re-usable buffer for readContinuedLineSlice +} + +// NewReader returns a new Reader reading from r. +// +// To avoid denial of service attacks, the provided bufio.Reader +// should be reading from an io.LimitReader or similar Reader to bound +// the size of responses. +func NewReader(r *bufio.Reader) *Reader { + return &Reader{R: r} +} + +// ReadLine reads a single line from r, +// eliding the final \n or \r\n from the returned string. +func (r *Reader) ReadLine() (string, error) { + line, err := r.readLineSlice() + return string(line), err +} + +// ReadLineBytes is like ReadLine but returns a []byte instead of a string. +func (r *Reader) ReadLineBytes() ([]byte, error) { + line, err := r.readLineSlice() + if line != nil { + buf := make([]byte, len(line)) + copy(buf, line) + line = buf + } + return line, err +} + +func (r *Reader) readLineSlice() ([]byte, error) { + r.closeDot() + var line []byte + for { + l, more, err := r.R.ReadLine() + if err != nil { + return nil, err + } + // Avoid the copy if the first call produced a full line. + if line == nil && !more { + return l, nil + } + line = append(line, l...) + if !more { + break + } + } + return line, nil +} + +// ReadContinuedLine reads a possibly continued line from r, +// eliding the final trailing ASCII white space. +// Lines after the first are considered continuations if they +// begin with a space or tab character. In the returned data, +// continuation lines are separated from the previous line +// only by a single space: the newline and leading white space +// are removed. +// +// For example, consider this input: +// +// Line 1 +// continued... +// Line 2 +// +// The first call to ReadContinuedLine will return "Line 1 continued..." +// and the second will return "Line 2". +// +// Empty lines are never continued. +func (r *Reader) ReadContinuedLine() (string, error) { + line, err := r.readContinuedLineSlice(noValidation) + return string(line), err +} + +// trim returns s with leading and trailing spaces and tabs removed. +// It does not assume Unicode or UTF-8. +func trim(s []byte) []byte { + i := 0 + for i < len(s) && (s[i] == ' ' || s[i] == '\t') { + i++ + } + n := len(s) + for n > i && (s[n-1] == ' ' || s[n-1] == '\t') { + n-- + } + return s[i:n] +} + +// ReadContinuedLineBytes is like ReadContinuedLine but +// returns a []byte instead of a string. +func (r *Reader) ReadContinuedLineBytes() ([]byte, error) { + line, err := r.readContinuedLineSlice(noValidation) + if line != nil { + buf := make([]byte, len(line)) + copy(buf, line) + line = buf + } + return line, err +} + +// readContinuedLineSlice reads continued lines from the reader buffer, +// returning a byte slice with all lines. The validateFirstLine function +// is run on the first read line, and if it returns an error then this +// error is returned from readContinuedLineSlice. +func (r *Reader) readContinuedLineSlice(validateFirstLine func([]byte) error) ([]byte, error) { + if validateFirstLine == nil { + return nil, errors.New("missing validateFirstLine func") + } + + // Read the first line. + line, err := r.readLineSlice() + if err != nil { + return nil, err + } + if len(line) == 0 { // blank line - no continuation + return line, nil + } + + if err := validateFirstLine(line); err != nil { + return nil, err + } + + // Optimistically assume that we have started to buffer the next line + // and it starts with an ASCII letter (the next header key), or a blank + // line, so we can avoid copying that buffered data around in memory + // and skipping over non-existent whitespace. + if r.R.Buffered() > 1 { + peek, _ := r.R.Peek(2) + if len(peek) > 0 && (isASCIILetter(peek[0]) || peek[0] == '\n') || + len(peek) == 2 && peek[0] == '\r' && peek[1] == '\n' { + return trim(line), nil + } + } + + // ReadByte or the next readLineSlice will flush the read buffer; + // copy the slice into buf. + r.buf = append(r.buf[:0], trim(line)...) + + // Read continuation lines. + for r.skipSpace() > 0 { + line, err := r.readLineSlice() + if err != nil { + break + } + r.buf = append(r.buf, ' ') + r.buf = append(r.buf, trim(line)...) + } + + //lint:ignore nilerr to maintain go stdlib compatibility. + return r.buf, nil +} + +// skipSpace skips R over all spaces and returns the number of bytes skipped. +func (r *Reader) skipSpace() int { + n := 0 + for { + c, err := r.R.ReadByte() + if err != nil { + // Bufio will keep err until next read. + break + } + if c != ' ' && c != '\t' { + _ = r.R.UnreadByte() + break + } + n++ + } + return n +} + +func (r *Reader) readCodeLine(expectCode int) (code int, continued bool, message string, err error) { + line, err := r.ReadLine() + if err != nil { + return + } + return parseCodeLine(line, expectCode) +} + +func parseCodeLine(line string, expectCode int) (code int, continued bool, message string, err error) { + if len(line) < 4 || line[3] != ' ' && line[3] != '-' { + err = textproto.ProtocolError("short response: " + line) + return + } + continued = line[3] == '-' + code, err = strconv.Atoi(line[0:3]) + if err != nil || code < 100 { + err = textproto.ProtocolError("invalid response code: " + line) + return + } + message = line[4:] + if 1 <= expectCode && expectCode < 10 && code/100 != expectCode || + 10 <= expectCode && expectCode < 100 && code/10 != expectCode || + 100 <= expectCode && expectCode < 1000 && code != expectCode { + err = &textproto.Error{Code: code, Msg: message} + } + return +} + +// ReadCodeLine reads a response code line of the form +// +// code message +// +// where code is a three-digit status code and the message +// extends to the rest of the line. An example of such a line is: +// +// 220 plan9.bell-labs.com ESMTP +// +// If the prefix of the status does not match the digits in expectCode, +// ReadCodeLine returns with err set to &Error{code, message}. +// For example, if expectCode is 31, an error will be returned if +// the status is not in the range [310,319]. +// +// If the response is multi-line, ReadCodeLine returns an error. +// +// An expectCode <= 0 disables the check of the status code. +func (r *Reader) ReadCodeLine(expectCode int) (code int, message string, err error) { + code, continued, message, err := r.readCodeLine(expectCode) + if err == nil && continued { + err = textproto.ProtocolError("unexpected multi-line response: " + message) + } + return +} + +// ReadResponse reads a multi-line response of the form: +// +// code-message line 1 +// code-message line 2 +// ... +// code message line n +// +// where code is a three-digit status code. The first line starts with the +// code and a hyphen. The response is terminated by a line that starts +// with the same code followed by a space. Each line in message is +// separated by a newline (\n). +// +// See page 36 of RFC 959 (https://www.ietf.org/rfc/rfc959.txt) for +// details of another form of response accepted: +// +// code-message line 1 +// message line 2 +// ... +// code message line n +// +// If the prefix of the status does not match the digits in expectCode, +// ReadResponse returns with err set to &Error{code, message}. +// For example, if expectCode is 31, an error will be returned if +// the status is not in the range [310,319]. +// +// An expectCode <= 0 disables the check of the status code. +func (r *Reader) ReadResponse(expectCode int) (code int, message string, err error) { + code, continued, message, err := r.readCodeLine(expectCode) + multi := continued + for continued { + line, err := r.ReadLine() + if err != nil { + return 0, "", err + } + + var code2 int + var moreMessage string + code2, continued, moreMessage, err = parseCodeLine(line, 0) + if err != nil || code2 != code { + message += "\n" + strings.TrimRight(line, "\r\n") + continued = true + continue + } + message += "\n" + moreMessage + } + if err != nil && multi && message != "" { + // replace one line error message with all lines (full message) + err = &textproto.Error{Code: code, Msg: message} + } + return +} + +// DotReader returns a new Reader that satisfies Reads using the +// decoded text of a dot-encoded block read from r. +// The returned Reader is only valid until the next call +// to a method on r. +// +// Dot encoding is a common framing used for data blocks +// in text protocols such as SMTP. The data consists of a sequence +// of lines, each of which ends in "\r\n". The sequence itself +// ends at a line containing just a dot: ".\r\n". Lines beginning +// with a dot are escaped with an additional dot to avoid +// looking like the end of the sequence. +// +// The decoded form returned by the Reader's Read method +// rewrites the "\r\n" line endings into the simpler "\n", +// removes leading dot escapes if present, and stops with error io.EOF +// after consuming (and discarding) the end-of-sequence line. +func (r *Reader) DotReader() io.Reader { + r.closeDot() + r.dot = &dotReader{r: r} + return r.dot +} + +type dotReader struct { + r *Reader + state int +} + +// Read satisfies reads by decoding dot-encoded data read from d.r. +func (d *dotReader) Read(b []byte) (n int, err error) { + // Run data through a simple state machine to + // elide leading dots, rewrite trailing \r\n into \n, + // and detect ending .\r\n line. + const ( + stateBeginLine = iota // beginning of line; initial state; must be zero + stateDot // read . at beginning of line + stateDotCR // read .\r at beginning of line + stateCR // read \r (possibly at end of line) + stateData // reading data in middle of line + stateEOF // reached .\r\n end marker line + ) + br := d.r.R + for n < len(b) && d.state != stateEOF { + var c byte + c, err = br.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + switch d.state { + case stateBeginLine: + if c == '.' { + d.state = stateDot + continue + } + if c == '\r' { + d.state = stateCR + continue + } + d.state = stateData + + case stateDot: + if c == '\r' { + d.state = stateDotCR + continue + } + if c == '\n' { + d.state = stateEOF + continue + } + d.state = stateData + + case stateDotCR: + if c == '\n' { + d.state = stateEOF + continue + } + // Not part of .\r\n. + // Consume leading dot and emit saved \r. + _ = br.UnreadByte() + c = '\r' + d.state = stateData + + case stateCR: + if c == '\n' { + d.state = stateBeginLine + break + } + // Not part of \r\n. Emit saved \r + _ = br.UnreadByte() + c = '\r' + d.state = stateData + + case stateData: + if c == '\r' { + d.state = stateCR + continue + } + if c == '\n' { + d.state = stateBeginLine + } + } + b[n] = c + n++ + } + if err == nil && d.state == stateEOF { + err = io.EOF + } + if err != nil && d.r.dot == d { + d.r.dot = nil + } + return +} + +// closeDot drains the current DotReader if any, +// making sure that it reads until the ending dot line. +func (r *Reader) closeDot() { + if r.dot == nil { + return + } + buf := make([]byte, 128) + for r.dot != nil { + // When Read reaches EOF or an error, + // it will set r.dot == nil. + _, _ = r.dot.Read(buf) + } +} + +// ReadDotBytes reads a dot-encoding and returns the decoded data. +// +// See the documentation for the DotReader method for details about dot-encoding. +func (r *Reader) ReadDotBytes() ([]byte, error) { + return io.ReadAll(r.DotReader()) +} + +// ReadDotLines reads a dot-encoding and returns a slice +// containing the decoded lines, with the final \r\n or \n elided from each. +// +// See the documentation for the DotReader method for details about dot-encoding. +func (r *Reader) ReadDotLines() ([]string, error) { + // We could use ReadDotBytes and then Split it, + // but reading a line at a time avoids needing a + // large contiguous block of memory and is simpler. + var v []string + var err error + for { + var line string + line, err = r.ReadLine() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + + // Dot by itself marks end; otherwise cut one dot. + if len(line) > 0 && line[0] == '.' { + if len(line) == 1 { + break + } + line = line[1:] + } + v = append(v, line) + } + return v, err +} + +var colon = []byte(":") + +// ReadMIMEHeader reads a MIME-style header from r. +// The header is a sequence of possibly continued Key: Value lines +// ending in a blank line. +// The returned map m maps CanonicalMIMEHeaderKey(key) to a +// sequence of values in the same order encountered in the input. +// +// For example, consider this input: +// +// My-Key: Value 1 +// Long-Key: Even +// Longer Value +// My-Key: Value 2 +// +// Given that input, ReadMIMEHeader returns the map: +// +// map[string][]string{ +// "My-Key": {"Value 1", "Value 2"}, +// "Long-Key": {"Even Longer Value"}, +// } +func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) { + return readMIMEHeader(r, math.MaxInt64) +} + +// readMIMEHeader is a version of ReadMIMEHeader which takes a limit on the header size. +// It is called by the mime/multipart package. +func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { + // Avoid lots of small slice allocations later by allocating one + // large one ahead of time which we'll cut up into smaller + // slices. If this isn't big enough later, we allocate small ones. + var strs []string + hint := r.upcomingHeaderNewlines() + if hint > 0 { + strs = make([]string, hint) + } + + m := make(MIMEHeader, hint) + + // The first line cannot start with a leading space. + if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') { + line, err := r.readLineSlice() + if err != nil { + return m, err + } + return m, textproto.ProtocolError("malformed MIME header initial line: " + string(line)) + } + + for { + kv, err := r.readContinuedLineSlice(mustHaveFieldNameColon) + if len(kv) == 0 { + return m, err + } + + // Key ends at first colon. + k, v, ok := bytes.Cut(kv, colon) + if !ok { + return m, textproto.ProtocolError("malformed MIME header line: " + string(kv)) + } + key, ok := canonicalMIMEHeaderKey(k) + if !ok { + return m, textproto.ProtocolError("malformed MIME header line: " + string(kv)) + } + for _, c := range v { + if !validHeaderValueByte(c) { + return m, textproto.ProtocolError("malformed MIME header line: " + string(kv)) + } + } + + // As per RFC 7230 field-name is a token, tokens consist of one or more chars. + // We could return a ProtocolError here, but better to be liberal in what we + // accept, so if we get an empty key, skip it. + if key == "" { + continue + } + + // Skip initial spaces in value. + value := string(bytes.TrimLeft(v, " \t")) + + vv := m[key] + if vv == nil { + lim -= int64(len(key)) + lim -= 100 // map entry overhead + } + lim -= int64(len(value)) + if lim < 0 { + // TODO: This should be a distinguishable error (ErrMessageTooLarge) + // to allow mime/multipart to detect it. + return m, errors.New("message too large") + } + if vv == nil && len(strs) > 0 { + // More than likely this will be a single-element key. + // Most headers aren't multi-valued. + // Set the capacity on strs[0] to 1, so any future append + // won't extend the slice into the other strings. + vv, strs = strs[:1:1], strs[1:] + vv[0] = value + m[key] = vv + } else { + m[key] = append(vv, value) + } + + if err != nil { + return m, err + } + } +} + +// noValidation is a no-op validation func for readContinuedLineSlice +// that permits any lines. +func noValidation(_ []byte) error { return nil } + +// mustHaveFieldNameColon ensures that, per RFC 7230, the +// field-name is on a single line, so the first line must +// contain a colon. +func mustHaveFieldNameColon(line []byte) error { + if bytes.IndexByte(line, ':') < 0 { + return textproto.ProtocolError(fmt.Sprintf("malformed MIME header: missing colon: %q", line)) + } + return nil +} + +var nl = []byte("\n") + +// upcomingHeaderNewlines returns an approximation of the number of newlines +// that will be in this header. If it gets confused, it returns 0. +func (r *Reader) upcomingHeaderNewlines() (n int) { + // Try to determine the 'hint' size. + _, _ = r.R.Peek(1) // force a buffer load if empty + s := r.R.Buffered() + if s == 0 { + return + } + peek, _ := r.R.Peek(s) + return bytes.Count(peek, nl) +} + +// CanonicalMIMEHeaderKey returns the canonical format of the +// MIME header key s. The canonicalization converts the first +// letter and any letter following a hyphen to upper case; +// the rest are converted to lowercase. For example, the +// canonical key for "accept-encoding" is "Accept-Encoding". +// MIME header keys are assumed to be ASCII only. +// If s contains a space or invalid header field bytes, it is +// returned without modifications. +func CanonicalMIMEHeaderKey(s string) string { + // Quick check for canonical encoding. + upper := true + for i := 0; i < len(s); i++ { + c := s[i] + if !validHeaderFieldByte(c) { + return s + } + if upper && 'a' <= c && c <= 'z' { + s, _ = canonicalMIMEHeaderKey([]byte(s)) + return s + } + if !upper && 'A' <= c && c <= 'Z' { + s, _ = canonicalMIMEHeaderKey([]byte(s)) + return s + } + upper = c == '-' + } + return s +} + +const toLower = 'a' - 'A' + +// validHeaderFieldByte reports whether c is a valid byte in a header +// field name. RFC 7230 says: +// +// header-field = field-name ":" OWS field-value OWS +// field-name = token +// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / +// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +// token = 1*tchar +func validHeaderFieldByte(c byte) bool { + // mask is a 128-bit bitmap with 1s for allowed bytes, + // so that the byte c can be tested with a shift and an and. + // If c >= 128, then 1<>64)) != 0 +} + +// validHeaderValueByte reports whether c is a valid byte in a header +// field value. RFC 7230 says: +// +// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] +// field-vchar = VCHAR / obs-text +// obs-text = %x80-FF +// +// RFC 5234 says: +// +// HTAB = %x09 +// SP = %x20 +// VCHAR = %x21-7E +func validHeaderValueByte(c byte) bool { + // mask is a 128-bit bitmap with 1s for allowed bytes, + // so that the byte c can be tested with a shift and an and. + // If c >= 128, then 1<>64)) == 0 +} + +// canonicalMIMEHeaderKey is like CanonicalMIMEHeaderKey but is +// allowed to mutate the provided byte slice before returning the +// string. +// +// For invalid inputs (if a contains spaces or non-token bytes), a +// is unchanged and a string copy is returned. +// +// ok is true if the header key contains only valid characters and spaces. +// ReadMIMEHeader accepts header keys containing spaces, but does not +// canonicalize them. +func canonicalMIMEHeaderKey(a []byte) (_ string, ok bool) { + // See if a looks like a header key. If not, return it unchanged. + noCanon := false + for _, c := range a { + if validHeaderFieldByte(c) { + continue + } + // Don't canonicalize. + if c == ' ' { + // We accept invalid headers with a space before the + // colon, but must not canonicalize them. + // See https://go.dev/issue/34540. + noCanon = true + continue + } + return string(a), false + } + if noCanon { + return string(a), true + } + + upper := true + for i, c := range a { + // Canonicalize: first letter upper case + // and upper case after each dash. + // (Host, User-Agent, If-Modified-Since). + // MIME headers are ASCII only, so no Unicode issues. + if upper && 'a' <= c && c <= 'z' { + c -= toLower + } else if !upper && 'A' <= c && c <= 'Z' { + c += toLower + } + a[i] = c + upper = c == '-' // for next time + } + commonHeaderOnce.Do(initCommonHeader) + // The compiler recognizes m[string(byteSlice)] as a special + // case, so a copy of a's bytes into a new string does not + // happen in this map lookup: + if v := commonHeader[string(a)]; v != "" { + return v, true + } + return string(a), true +} + +// commonHeader interns common header strings. +var commonHeader map[string]string + +var commonHeaderOnce sync.Once + +func initCommonHeader() { + commonHeader = make(map[string]string) + for _, v := range []string{ + "Accept", + "Accept-Charset", + "Accept-Encoding", + "Accept-Language", + "Accept-Ranges", + "Cache-Control", + "Cc", + "Connection", + "Content-Id", + "Content-Language", + "Content-Length", + "Content-Transfer-Encoding", + "Content-Type", + "Cookie", + "Date", + "Dkim-Signature", + "Etag", + "Expires", + "From", + "Host", + "If-Modified-Since", + "If-None-Match", + "In-Reply-To", + "Last-Modified", + "Location", + "Message-Id", + "Mime-Version", + "Pragma", + "Received", + "Return-Path", + "Server", + "Set-Cookie", + "Subject", + "To", + "User-Agent", + "Via", + "X-Forwarded-For", + "X-Imforwards", + "X-Powered-By", + } { + commonHeader[v] = v + } +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/reader_email.go b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/reader_email.go new file mode 100644 index 000000000..0b49509f3 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/reader_email.go @@ -0,0 +1,214 @@ +package textproto + +import ( + "bytes" + "errors" + "math" + "net/textproto" +) + +// ReadEmailMIMEHeader reads a MIME-style header from r. +// +// This is a modified version of the stock func that better handles the characters +// we must support in email, instead of just HTTP. +func (r *Reader) ReadEmailMIMEHeader() (MIMEHeader, error) { + return readEmailMIMEHeader(r, math.MaxInt64) +} + +func readEmailMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { + // Avoid lots of small slice allocations later by allocating one + // large one ahead of time which we'll cut up into smaller + // slices. If this isn't big enough later, we allocate small ones. + var strs []string + hint := r.upcomingHeaderNewlines() + if hint > 0 { + strs = make([]string, hint) + } + + m := make(MIMEHeader, hint) + + // The first line cannot start with a leading space. + if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') { + line, err := r.readLineSlice() + if err != nil { + return m, err + } + return m, textproto.ProtocolError("malformed MIME header initial line: " + string(line)) + } + + for { + kv, err := r.readContinuedLineSlice(mustHaveFieldNameColon) + if len(kv) == 0 { + return m, err + } + + // Key ends at first colon. + k, v, ok := bytes.Cut(kv, colon) + if !ok { + return m, textproto.ProtocolError("malformed MIME header line: " + string(kv)) + } + key, ok := canonicalEmailMIMEHeaderKey(k) + if !ok { + return m, textproto.ProtocolError("malformed MIME header line: " + string(kv)) + } + // for _, c := range v { + // if !validHeaderValueByte(c) { + // return m, ProtocolError("malformed MIME header line: " + string(kv)) + // } + // } + + // As per RFC 7230 field-name is a token, tokens consist of one or more chars. + // We could return a ProtocolError here, but better to be liberal in what we + // accept, so if we get an empty key, skip it. + if key == "" { + continue + } + + // Skip initial spaces in value. + value := string(bytes.TrimLeft(v, " \t")) + + vv := m[key] + if vv == nil { + lim -= int64(len(key)) + lim -= 100 // map entry overhead + } + lim -= int64(len(value)) + if lim < 0 { + // TODO: This should be a distinguishable error (ErrMessageTooLarge) + // to allow mime/multipart to detect it. + return m, errors.New("message too large") + } + if vv == nil && len(strs) > 0 { + // More than likely this will be a single-element key. + // Most headers aren't multi-valued. + // Set the capacity on strs[0] to 1, so any future append + // won't extend the slice into the other strings. + vv, strs = strs[:1:1], strs[1:] + vv[0] = value + m[key] = vv + } else { + m[key] = append(vv, value) + } + + if err != nil { + return m, err + } + } +} + +// CanonicalEmailMIMEHeaderKey returns the canonical format of the +// MIME header key s. +// +// This is a modified version of the stock func that better handles the characters +// we must support in email, instead of just HTTP. +func CanonicalEmailMIMEHeaderKey(s string) string { + // Quick check for canonical encoding. + upper := true + for i := 0; i < len(s); i++ { + c := s[i] + if !ValidEmailHeaderFieldByte(c) { + return s + } + if upper && 'a' <= c && c <= 'z' { + s, _ = canonicalEmailMIMEHeaderKey([]byte(s)) + return s + } + if !upper && 'A' <= c && c <= 'Z' { + s, _ = canonicalEmailMIMEHeaderKey([]byte(s)) + return s + } + upper = c == '-' + } + return s +} + +func canonicalEmailMIMEHeaderKey(a []byte) (_ string, ok bool) { + noCanon := false + for _, c := range a { + if ValidEmailHeaderFieldByte(c) { + continue + } + // Don't canonicalize. + if c == ' ' { + // We accept invalid headers with a space before the + // colon, but must not canonicalize them. + // See https://go.dev/issue/34540. + noCanon = true + continue + } + return string(a), false + } + if noCanon { + return string(a), true + } + + upper := true + for i, c := range a { + // Canonicalize: first letter upper case + // and upper case after each dash. + // (Host, User-Agent, If-Modified-Since). + // MIME headers are ASCII only, so no Unicode issues. + if upper && 'a' <= c && c <= 'z' { + c -= toLower + } else if !upper && 'A' <= c && c <= 'Z' { + c += toLower + } + a[i] = c + upper = c == '-' // for next time + } + commonHeaderOnce.Do(initCommonHeader) + // The compiler recognizes m[string(byteSlice)] as a special + // case, so a copy of a's bytes into a new string does not + // happen in this map lookup: + if v := commonHeader[string(a)]; v != "" { + return v, true + } + return string(a), true +} + +// ValidEmailHeaderFieldByte Valid characters in email header field. +// +// According to [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322#section-2.2), +// +// > A field name MUST be composed of printable US-ASCII characters (i.e., +// > characters that have values between 33 and 126, inclusive), except +// > colon. +func ValidEmailHeaderFieldByte(c byte) bool { + const mask = 0 | + (1<<(10)-1)<<'0' | + (1<<(26)-1)<<'a' | + (1<<(26)-1)<<'A' | + 1<<'!' | + 1<<'"' | + 1<<'#' | + 1<<'$' | + 1<<'%' | + 1<<'&' | + 1<<'\'' | + 1<<'(' | + 1<<')' | + 1<<'*' | + 1<<'+' | + 1<<',' | + 1<<'-' | + 1<<'.' | + 1<<'/' | + 1<<';' | + 1<<'<' | + 1<<'=' | + 1<<'>' | + 1<<'?' | + 1<<'@' | + 1<<'[' | + 1<<'\\' | + 1<<']' | + 1<<'^' | + 1<<'_' | + 1<<'`' | + 1<<'{' | + 1<<'|' | + 1<<'}' | + 1<<'~' + return ((uint64(1)<>64)) != 0 +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/textproto.go b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/textproto.go new file mode 100644 index 000000000..0d92bf26a --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/textproto.go @@ -0,0 +1,133 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package textproto implements generic support for text-based request/response +// protocols in the style of HTTP, NNTP, and SMTP. +// +// The package provides: +// +// Error, which represents a numeric error response from +// a server. +// +// Pipeline, to manage pipelined requests and responses +// in a client. +// +// Reader, to read numeric response code lines, +// key: value headers, lines wrapped with leading spaces +// on continuation lines, and whole text blocks ending +// with a dot on a line by itself. +// +// Writer, to write dot-encoded text blocks. +// +// Conn, a convenient packaging of Reader, Writer, and Pipeline for use +// with a single network connection. +package textproto + +import ( + "bufio" + "io" + "net" +) + +// A Conn represents a textual network protocol connection. +// It consists of a Reader and Writer to manage I/O +// and a Pipeline to sequence concurrent requests on the connection. +// These embedded types carry methods with them; +// see the documentation of those types for details. +type Conn struct { + Reader + Writer + Pipeline + conn io.ReadWriteCloser +} + +// NewConn returns a new Conn using conn for I/O. +func NewConn(conn io.ReadWriteCloser) *Conn { + return &Conn{ + Reader: Reader{R: bufio.NewReader(conn)}, + Writer: Writer{W: bufio.NewWriter(conn)}, + conn: conn, + } +} + +// Close closes the connection. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// Dial connects to the given address on the given network using net.Dial +// and then returns a new Conn for the connection. +func Dial(network, addr string) (*Conn, error) { + c, err := net.Dial(network, addr) + if err != nil { + return nil, err + } + return NewConn(c), nil +} + +// Cmd is a convenience method that sends a command after +// waiting its turn in the pipeline. The command text is the +// result of formatting format with args and appending \r\n. +// Cmd returns the id of the command, for use with StartResponse and EndResponse. +// +// For example, a client might run a HELP command that returns a dot-body +// by using: +// +// id, err := c.Cmd("HELP") +// if err != nil { +// return nil, err +// } +// +// c.StartResponse(id) +// defer c.EndResponse(id) +// +// if _, _, err = c.ReadCodeLine(110); err != nil { +// return nil, err +// } +// text, err := c.ReadDotBytes() +// if err != nil { +// return nil, err +// } +// return c.ReadCodeLine(250) +func (c *Conn) Cmd(format string, args ...any) (id uint, err error) { + id = c.Next() + c.StartRequest(id) + err = c.PrintfLine(format, args...) + c.EndRequest(id) + if err != nil { + return 0, err + } + return id, nil +} + +// TrimString returns s without leading and trailing ASCII space. +func TrimString(s string) string { + for len(s) > 0 && isASCIISpace(s[0]) { + s = s[1:] + } + for len(s) > 0 && isASCIISpace(s[len(s)-1]) { + s = s[:len(s)-1] + } + return s +} + +// TrimBytes returns b without leading and trailing ASCII space. +func TrimBytes(b []byte) []byte { + for len(b) > 0 && isASCIISpace(b[0]) { + b = b[1:] + } + for len(b) > 0 && isASCIISpace(b[len(b)-1]) { + b = b[:len(b)-1] + } + return b +} + +func isASCIISpace(b byte) bool { + return b == ' ' || b == '\t' || b == '\n' || b == '\r' +} + +func isASCIILetter(b byte) bool { + b |= 0x20 // make lower case + return 'a' <= b && b <= 'z' +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/writer.go b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/writer.go new file mode 100644 index 000000000..5aed2a02e --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/internal/textproto/writer.go @@ -0,0 +1,119 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package textproto + +import ( + "bufio" + "fmt" + "io" +) + +// A Writer implements convenience methods for writing +// requests or responses to a text protocol network connection. +type Writer struct { + W *bufio.Writer + dot *dotWriter +} + +// NewWriter returns a new Writer writing to w. +func NewWriter(w *bufio.Writer) *Writer { + return &Writer{W: w} +} + +var crnl = []byte{'\r', '\n'} +var dotcrnl = []byte{'.', '\r', '\n'} + +// PrintfLine writes the formatted output followed by \r\n. +func (w *Writer) PrintfLine(format string, args ...any) error { + w.closeDot() + fmt.Fprintf(w.W, format, args...) + _, _ = w.W.Write(crnl) + return w.W.Flush() +} + +// DotWriter returns a writer that can be used to write a dot-encoding to w. +// It takes care of inserting leading dots when necessary, +// translating line-ending \n into \r\n, and adding the final .\r\n line +// when the DotWriter is closed. The caller should close the +// DotWriter before the next call to a method on w. +// +// See the documentation for Reader's DotReader method for details about dot-encoding. +func (w *Writer) DotWriter() io.WriteCloser { + w.closeDot() + w.dot = &dotWriter{w: w} + return w.dot +} + +func (w *Writer) closeDot() { + if w.dot != nil { + w.dot.Close() // sets w.dot = nil + } +} + +type dotWriter struct { + w *Writer + state int +} + +const ( + wstateBegin = iota // initial state; must be zero + wstateBeginLine // beginning of line + wstateCR // wrote \r (possibly at end of line) + wstateData // writing data in middle of line +) + +func (d *dotWriter) Write(b []byte) (n int, err error) { + bw := d.w.W + for n < len(b) { + c := b[n] + switch d.state { + case wstateBegin, wstateBeginLine: + d.state = wstateData + if c == '.' { + // escape leading dot + _ = bw.WriteByte('.') + } + fallthrough + + case wstateData: + if c == '\r' { + d.state = wstateCR + } + if c == '\n' { + _ = bw.WriteByte('\r') + d.state = wstateBeginLine + } + + case wstateCR: + d.state = wstateData + if c == '\n' { + d.state = wstateBeginLine + } + } + if err = bw.WriteByte(c); err != nil { + break + } + n++ + } + return +} + +func (d *dotWriter) Close() error { + if d.w.dot == d { + d.w.dot = nil + } + bw := d.w.W + switch d.state { + default: + _ = bw.WriteByte('\r') + fallthrough + case wstateCR: + _ = bw.WriteByte('\n') + fallthrough + case wstateBeginLine: + _, _ = bw.Write(dotcrnl) + } + return bw.Flush() +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/match.go b/vendor/github.com/jhillyerd/enmime/v2/match.go new file mode 100644 index 000000000..c8f7d5934 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/match.go @@ -0,0 +1,107 @@ +package enmime + +import ( + "container/list" +) + +// PartMatcher is a function type that you must implement to search for Parts using the +// BreadthMatch* functions. Implementators should inspect the provided Part and return true if it +// matches your criteria. +type PartMatcher func(part *Part) bool + +// BreadthMatchFirst performs a breadth first search of the Part tree and returns the first part +// that causes the given matcher to return true +func (p *Part) BreadthMatchFirst(matcher PartMatcher) *Part { + q := list.New() + q.PushBack(p) + + // Push children onto queue and attempt to match in that order + for q.Len() > 0 { + e := q.Front() + p := e.Value.(*Part) + if matcher(p) { + return p + } + q.Remove(e) + c := p.FirstChild + for c != nil { + q.PushBack(c) + c = c.NextSibling + } + } + + return nil +} + +// BreadthMatchAll performs a breadth first search of the Part tree and returns all parts that cause +// the given matcher to return true +func (p *Part) BreadthMatchAll(matcher PartMatcher) []*Part { + q := list.New() + q.PushBack(p) + + matches := make([]*Part, 0, 10) + + // Push children onto queue and attempt to match in that order + for q.Len() > 0 { + e := q.Front() + p := e.Value.(*Part) + if matcher(p) { + matches = append(matches, p) + } + q.Remove(e) + c := p.FirstChild + for c != nil { + q.PushBack(c) + c = c.NextSibling + } + } + + return matches +} + +// DepthMatchFirst performs a depth first search of the Part tree and returns the first part that +// causes the given matcher to return true +func (p *Part) DepthMatchFirst(matcher PartMatcher) *Part { + root := p + for { + if matcher(p) { + return p + } + c := p.FirstChild + if c != nil { + p = c + } else { + for p.NextSibling == nil { + if p == root { + return nil + } + p = p.Parent + } + p = p.NextSibling + } + } +} + +// DepthMatchAll performs a depth first search of the Part tree and returns all parts that causes +// the given matcher to return true +func (p *Part) DepthMatchAll(matcher PartMatcher) []*Part { + root := p + matches := make([]*Part, 0, 10) + for { + if matcher(p) { + matches = append(matches, p) + } + c := p.FirstChild + if c != nil { + p = c + } else { + for p.NextSibling == nil { + if p == root { + return matches + } + p = p.Parent + } + p = p.NextSibling + } + } +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/mediatype/mediatype.go b/vendor/github.com/jhillyerd/enmime/v2/mediatype/mediatype.go new file mode 100644 index 000000000..c2b49b49b --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/mediatype/mediatype.go @@ -0,0 +1,549 @@ +package mediatype + +import ( + "fmt" + "mime" + "strings" + _utf8 "unicode/utf8" + + "github.com/jhillyerd/enmime/v2/internal/coding" + "github.com/jhillyerd/enmime/v2/internal/stringutil" + "github.com/pkg/errors" +) + +const ( + // Standard MIME content types + ctAppPrefix = "application/" + ctAppOctetStream = "application/octet-stream" + ctMultipartMixed = "multipart/mixed" + ctMultipartPrefix = "multipart/" + ctTextPrefix = "text/" + ctTextPlain = "text/plain" + + // Used as a placeholder in case of malformed Content-Type headers + ctPlaceholder = "x-not-a-mime-type/x-not-a-mime-type" + // Used as a placeholder param value in case of malformed + // Content-Type/Content-Disposition parameters that lack values. + // E.g.: Content-Type: text/html;iso-8859-1 + pvPlaceholder = "not-a-param-value" + + utf8 = "utf-8" +) + +// ParseOptions controls the parsing of content-type and media-type strings. +type ParseOptions struct { + StripMediaTypeInvalidCharacters bool +} + +// Parse is a more tolerant implementation of Go's mime.ParseMediaType function. +// +// Tolerances accounted for: +// - Missing ';' between content-type and media parameters +// - Repeating media parameters +// - Unquoted values in media parameters containing 'tspecials' characters +// - Newline characters +func Parse(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) { + return ParseWithOptions(ctype, ParseOptions{}) +} + +// ParseWithOptions parses media-type with additional options controlling the parsing behavior. +func ParseWithOptions(ctype string, options ParseOptions) (mtype string, params map[string]string, invalidParams []string, err error) { + mtype, params, err = mime.ParseMediaType( + fixNewlines(fixUnescapedQuotes(fixUnquotedSpecials(fixMangledMediaType(removeTrailingHTMLTags(ctype), ';', options))))) + if err != nil { + if err.Error() == "mime: no media type" { + return "", nil, nil, nil + } + return "", nil, nil, errors.WithStack(err) + } + + if mtype == ctPlaceholder { + mtype = "" + } + for name, value := range params { + if value != pvPlaceholder { + continue + } + invalidParams = append(invalidParams, name) + delete(params, name) + } + + return mtype, params, invalidParams, err +} + +// fixMangledMediaType is used to insert ; separators into media type strings that lack them, and +// remove repeated parameters. +func fixMangledMediaType(mtype string, sep rune, options ParseOptions) string { + strsep := string([]rune{sep}) + if mtype == "" { + return "" + } + + parts := stringutil.SplitUnquoted(mtype, sep, '"') + mtype = "" + if strings.Contains(parts[0], "=") { + // A parameter pair at this position indicates we are missing a content-type. + parts[0] = fmt.Sprintf("%s%s %s", ctAppOctetStream, strsep, parts[0]) + parts = strings.Split(strings.Join(parts, strsep), strsep) + } + + for i, p := range parts { + switch i { + case 0: + if p == "" { + // The content type is completely missing. Put in a placeholder. + p = ctPlaceholder + } + // Remove invalid characters (specials) + if options.StripMediaTypeInvalidCharacters { + p = removeTypeSpecials(p) + } + // Check for missing token after slash. + if strings.HasSuffix(p, "/") { + switch p { + case ctTextPrefix: + p = ctTextPlain + case ctAppPrefix: + p = ctAppOctetStream + case ctMultipartPrefix: + p = ctMultipartMixed + default: + // Safe default + p = ctAppOctetStream + } + } + // Remove extra ctype parts + if strings.Count(p, "/") > 1 { + ps := strings.SplitN(p, "/", 3) + p = strings.Join(ps[0:2], "/") + } + default: + if len(p) == 0 { + // Ignore trailing separators. + continue + } + + if len(strings.TrimSpace(p)) == 0 { + // Ignore empty parameters. + continue + } + + if !strings.Contains(p, "=") { + p = p + "=" + pvPlaceholder + } + + // RFC-2047 encoded attribute name. + p = coding.RFC2047Decode(p) + + pair := strings.SplitAfter(p, "=") + + if strings.TrimSpace(pair[0]) == "=" { + // Ignore unnamed parameters. + continue + } + + if strings.Contains(mtype, strings.TrimSpace(pair[0])) { + // Ignore repeated parameters. + continue + } + + if strings.ContainsAny(pair[0], "()<>@,;:\"\\/[]?") { + // Attribute is a strict token and cannot be a quoted-string. If any of the above + // characters are present in a token it must be quoted and is therefor an invalid + // attribute. Discard the pair. + continue + } + } + + mtype += p + + // Only terminate with semicolon if not the last parameter and if it doesn't already have a + // semicolon. + if i != len(parts)-1 && !strings.HasSuffix(mtype, ";") { + // Remove whitespace between parameter=value and ; + mtype = strings.TrimRight(mtype, " \t") + mtype += ";" + } + } + + mtype = strings.TrimSuffix(mtype, ";") + + return mtype +} + +// consumeParam takes the the parameter part of a Content-Type header, returns a clean version of +// the first parameter (quoted as necessary), and the remainder of the parameter part of the +// Content-Type header. +// +// Given this this header: +// +// `Content-Type: text/calendar; charset=utf-8; method=text/calendar` +// +// `consumeParams` should be given this part: +// +// ` charset=utf-8; method=text/calendar` +// +// And returns (first pass): +// +// `consumed = "charset=utf-8;"` +// `rest = " method=text/calendar"` +// +// Capture the `consumed` value (to build a clean Content-Type header value) and pass the value of +// `rest` back to `consumeParam`. That second call will return: +// +// `consumed = " method=\"text/calendar\""` +// `rest = ""` +// +// Again, use the value of `consumed` to build a clean Content-Type header value. Given that `rest` +// is empty, all of the parameters have been consumed successfully. +// +// If `consumed` is returned empty and `rest` is not empty, then the value of `rest` does not +// begin with a parsable parameter. This does not necessarily indicate a problem. For example, +// if there is trailing whitespace, it would be returned here. +func consumeParam(s string) (consumed, rest string) { + i := strings.IndexByte(s, '=') + if i < 0 { + return "", s + } + + // Write out parameter name. + param := strings.Builder{} + param.WriteString(s[:i+1]) + s = s[i+1:] + + value := strings.Builder{} + valueQuotedOriginally := false + valueQuoteAdded := false + valueQuoteNeeded := false + rfc2047Needed := false + + var r rune +findValueStart: + for i, r = range s { + switch r { + case ' ', '\t': + // Do not preserve leading whitespace. + + case '"': + valueQuotedOriginally = true + valueQuoteAdded = true + valueQuoteNeeded = true + param.WriteRune(r) + + break findValueStart + + case ';': + if value.Len() == 0 { + // Value was empty, return immediately. + param.WriteString(`"";`) + return param.String(), s[i+1:] + } + + break findValueStart + + default: + if r > 127 { + rfc2047Needed = true + } + + valueQuotedOriginally = false + valueQuoteAdded = false + value.WriteRune(r) + + break findValueStart + } + } + + quoteIfUnquoted := func() { + if !valueQuoteNeeded { + if !valueQuoteAdded { + param.WriteByte('"') + + valueQuoteAdded = true + } + + valueQuoteNeeded = true + } + } + + if len(s)-i < 1 { + // Parameter value starts at the end of the string, make empty + // quoted string to play nice with mime.ParseMediaType. + param.WriteString(`""`) + } else { + // The beginning of the value is not at the end of the string. + for _, v := range []byte{'(', ')', '<', '>', '@', ',', ':', '/', '[', ']', '?', '='} { + if s[0] == v { + quoteIfUnquoted() + break + } + } + + _, runeLength := _utf8.DecodeRuneInString(s[i:]) + s = s[i+runeLength:] + escaped := r == '\\' + + findValueEnd: + for i, r = range s { + if escaped { + value.WriteRune(r) + escaped = false + continue + } + + switch r { + case ';': + if valueQuotedOriginally { + // We're in a quoted string, so whitespace is allowed. + value.WriteRune(r) + break + } + + // Otherwise, we've reached the end of an unquoted value. + rest = s[i:] + break findValueEnd + + case ' ', '\t': + if valueQuotedOriginally { + // We're in a quoted string, so whitespace is allowed. + value.WriteRune(r) + break + } + + // This string contains whitespace, must be quoted. + quoteIfUnquoted() + value.WriteRune(r) + + case '"': + if valueQuotedOriginally { + // We're in a quoted value. This is the end of that value. + rest = s[i:] + break findValueEnd + } + + quoteIfUnquoted() + value.WriteByte('\\') + value.WriteRune(r) + + case '\\': + if i < len(s)-1 { + // If next char is present, escape it with backslash. + value.WriteRune(r) + escaped = true + quoteIfUnquoted() + } + + case '(', ')', '<', '>', '@', ',', ':', '/', '[', ']', '?', '=': + quoteIfUnquoted() + fallthrough + + default: + if r > 127 { + rfc2047Needed = true + } + value.WriteRune(r) + } + } + } + + if value.Len() > 0 { + // Convert whole value to RFC2047 if it contains forbidden characters (ASCII > 127) + val := value.String() + if rfc2047Needed { + val = mime.BEncoding.Encode(utf8, val) + // RFC 2047 must be quoted + quoteIfUnquoted() + } + + // Write the value + param.WriteString(val) + } + + // Add final quote if required + if valueQuoteNeeded { + param.WriteByte('"') + } + + // Write last parsed char if any + if rest != "" { + if rest[0] != '"' { + // When last char is quote, valueQuotedOriginally is surely true and the quote was already written. + // Otherwise output the character (; for example) + param.WriteByte(rest[0]) + } + + // Focus the rest of the string + rest = rest[1:] + } + + return param.String(), rest +} + +// fixUnquotedSpecials as defined in RFC 2045, section 5.1: +// https://tools.ietf.org/html/rfc2045#section-5.1 +func fixUnquotedSpecials(s string) string { + idx := strings.IndexByte(s, ';') + if idx < 0 || idx == len(s) { + // No parameters + return s + } + + clean := strings.Builder{} + clean.WriteString(s[:idx+1]) + s = s[idx+1:] + + for len(s) > 0 { + var consumed string + consumed, s = consumeParam(s) + + if len(consumed) == 0 { + clean.WriteString(s) + return clean.String() + } + + clean.WriteString(consumed) + } + + return clean.String() +} + +// fixUnescapedQuotes inspects for unescaped quotes inside of a quoted string and escapes them +// +// Input: application/rtf; charset=iso-8859-1; name=""V047411.rtf".rtf" +// Output: application/rtf; charset=iso-8859-1; name="\"V047411.rtf\".rtf" +func fixUnescapedQuotes(hvalue string) string { + params := stringutil.SplitAfterUnquoted(hvalue, ';', '"') + sb := &strings.Builder{} + + for i := 0; i < len(params); i++ { + // Inspect for "=" byte. + eq := strings.IndexByte(params[i], '=') + if eq < 0 { + // No "=", must be the content-type or a comment. + sb.WriteString(params[i]) + continue + } + sb.WriteString(params[i][:eq]) + param := params[i][eq:] + startingQuote := strings.IndexByte(param, '"') + closingQuote := strings.LastIndexByte(param, '"') + + // Opportunity to exit early if there are no quotes. + if startingQuote < 0 && closingQuote < 0 { + // This value is not quoted, write the value and carry on. + sb.WriteString(param) + continue + } + + // Check if only one quote was found in the string. + if closingQuote == startingQuote { + // Append the next chunk of params here in case of a semicolon mid string. + if len(params) > i+1 { + param = fmt.Sprintf("%s%s", param, params[i+1]) + } + closingQuote = strings.LastIndexByte(param, '"') + i++ + if closingQuote == startingQuote { + sb.WriteString("=\"\"") + return sb.String() + } + } + + // Write the k/v separator back in along with everything up until the first quote. + sb.WriteByte('=') + sb.WriteByte('"') // Starting quote + sb.WriteString(param[1:startingQuote]) + + // Get the value, less the outer quotes. + rest := param[closingQuote+1:] + + // If there is stuff after the last quote then we should escape the first quote. + if len(rest) > 0 && rest != ";" { + sb.WriteString("\\\"") + } + + param = param[startingQuote+1 : closingQuote] + escaped := false + for strIdx := range []byte(param) { + switch param[strIdx] { + case '"': + // We are inside of a quoted string, so lets escape this guy if it isn't already escaped. + if !escaped { + sb.WriteByte('\\') + escaped = false + } + sb.WriteByte(param[strIdx]) + case '\\': + // Something is getting escaped, a quote is the only char that needs + // this, so lets assume the following char is a double-quote. + escaped = true + sb.WriteByte('\\') + default: + escaped = false + sb.WriteByte(param[strIdx]) + } + } + + // If there is stuff after the last quote then we should escape the last quote, apply the + // rest and terminate with a quote. + switch rest { + case ";": + sb.WriteByte('"') + sb.WriteString(rest) + case "": + sb.WriteByte('"') + default: + sb.WriteByte('\\') + sb.WriteByte('"') + sb.WriteString(rest) + sb.WriteByte('"') + } + } + + return sb.String() +} + +// fixNewlines replaces \n with a space and removes \r +func fixNewlines(value string) string { + value = strings.ReplaceAll(value, "\n", " ") + value = strings.ReplaceAll(value, "\r", "") + return value +} + +// removeTrailingHTMLTags removes an unexpected HTML tags at the end of media type. +func removeTrailingHTMLTags(value string) string { + tagStart := 0 + closeTags := 0 + +loop: + for i := len(value) - 1; i > 0; i-- { + c := value[i] + switch c { + case '"': + if closeTags == 0 { // quotes started outside the tag, aborting + break loop + } + case '>': + closeTags++ + case '<': + closeTags-- + if closeTags == 0 { + tagStart = i + } + } + } + + if tagStart != 0 { + return value[0:tagStart] + } + + return value +} + +func removeTypeSpecials(value string) string { + for _, r := range []string{"(", ")", "<", ">", "@", ",", ":", "\\", "\"", "[", "]", "?", "="} { + value = strings.ReplaceAll(value, r, "") + } + + return value +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/options.go b/vendor/github.com/jhillyerd/enmime/v2/options.go new file mode 100644 index 000000000..9291d8b5a --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/options.go @@ -0,0 +1,126 @@ +package enmime + +// Option to configure parsing. +type Option interface { + apply(p *Parser) +} + +// SkipMalformedParts sets parsing to skip parts that's can't be parsed. +func SkipMalformedParts(s bool) Option { + return skipMalformedPartsOption(s) +} + +type skipMalformedPartsOption bool + +func (o skipMalformedPartsOption) apply(p *Parser) { + p.skipMalformedParts = bool(o) +} + +// MultipartWOBoundaryAsSinglePart if set to true will treat a multi-part messages without boundary parameter as single-part. +// Otherwise, will return error that boundary is not found. +func MultipartWOBoundaryAsSinglePart(a bool) Option { + return multipartWOBoundaryAsSinglePartOption(a) +} + +type multipartWOBoundaryAsSinglePartOption bool + +func (o multipartWOBoundaryAsSinglePartOption) apply(p *Parser) { + p.multipartWOBoundaryAsSinglePart = bool(o) +} + +// SetReadPartErrorPolicy sets the given callback function to readPartErrorPolicy. +func SetReadPartErrorPolicy(f ReadPartErrorPolicy) Option { + return readPartErrorPolicyOption(f) +} + +type readPartErrorPolicyOption ReadPartErrorPolicy + +func (o readPartErrorPolicyOption) apply(p *Parser) { + p.readPartErrorPolicy = ReadPartErrorPolicy(o) +} + +// MaxStoredPartErrors limits number of part parsing errors, errors beyond the limit are discarded. +// Zero, the default, means all errors will be kept. +func MaxStoredPartErrors(n int) Option { + return maxStoredPartErrorsOption(n) +} + +type maxStoredPartErrorsOption int + +func (o maxStoredPartErrorsOption) apply(p *Parser) { + p.maxStoredPartErrors = int(o) +} + +// RawContent if set to true will not try to decode the CTE and return the raw part content. +// Otherwise, will try to automatically decode the CTE. +func RawContent(a bool) Option { + return rawContentOption(a) +} + +type rawContentOption bool + +func (o rawContentOption) apply(p *Parser) { + p.rawContent = bool(o) +} + +// SetCustomParseMediaType if provided, will be used to parse media type instead of the default ParseMediaType +// function. This may be used to parse media type parameters that would otherwise be considered malformed. +// By default parsing happens using ParseMediaType +func SetCustomParseMediaType(customParseMediaType CustomParseMediaType) Option { + return parseMediaTypeOption(customParseMediaType) +} + +type parseMediaTypeOption CustomParseMediaType + +func (o parseMediaTypeOption) apply(p *Parser) { + p.customParseMediaType = CustomParseMediaType(o) +} + +type stripMediaTypeInvalidCharactersOption bool + +func (o stripMediaTypeInvalidCharactersOption) apply(p *Parser) { + p.stripMediaTypeInvalidCharacters = bool(o) +} + +// StripMediaTypeInvalidCharacters sets stripMediaTypeInvalidCharacters option. If true, invalid characters +// will be removed from media type during parsing. +func StripMediaTypeInvalidCharacters(stripMediaTypeInvalidCharacters bool) Option { + return stripMediaTypeInvalidCharactersOption(stripMediaTypeInvalidCharacters) +} + +type disableTextConversionOption bool + +func (o disableTextConversionOption) apply(p *Parser) { + p.disableTextConversion = bool(o) +} + +// DisableTextConversion sets the disableTextConversion option. When true, there will be no +// automated down conversion of HTML to text when a plain/text body is missing. +func DisableTextConversion(disableTextConversion bool) Option { + return disableTextConversionOption(disableTextConversion) +} + +type disableCharacterDetectionOption bool + +func (o disableCharacterDetectionOption) apply(p *Parser) { + p.disableCharacterDetection = bool(o) +} + +// DisableCharacterDetection sets the disableCharacterDetection option. When true, the parser will use the +// defined character set if it is defined in the message part. +func DisableCharacterDetection(disableCharacterDetection bool) Option { + return disableCharacterDetectionOption(disableCharacterDetection) +} + +type minCharsetDetectRunesOption int + +func (o minCharsetDetectRunesOption) apply(p *Parser) { + p.minCharsetDetectRunes = int(o) +} + +// MinCharsetDetectRunes sets the minimum length of a MIME part before enmime will attempt to +// detect its character set. The shorter the text, the more likely an incorrect character set +// will be chosen. The default is 100. +func MinCharsetDetectRunes(minCharsetDetectRunes int) Option { + return minCharsetDetectRunesOption(minCharsetDetectRunes) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/parser.go b/vendor/github.com/jhillyerd/enmime/v2/parser.go new file mode 100644 index 000000000..fb537d47f --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/parser.go @@ -0,0 +1,48 @@ +package enmime + +// ReadPartErrorPolicy allows to recover the buffer (or not) on an error when reading a Part content. +// +// See AllowCorruptTextPartErrorPolicy for usage. +type ReadPartErrorPolicy func(*Part, error) bool + +// AllowCorruptTextPartErrorPolicy recovers partial content from base64.CorruptInputError when content type is text/plain or text/html. +func AllowCorruptTextPartErrorPolicy(p *Part, err error) bool { + if IsBase64CorruptInputError(err) && (p.ContentType == ctTextHTML || p.ContentType == ctTextPlain) { + return true + } + return false +} + +// CustomParseMediaType parses media type. See ParseMediaType for more details +type CustomParseMediaType func(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) + +// Parser parses MIME. Create with NewParser to inherit recommended defaults. +type Parser struct { + maxStoredPartErrors int + multipartWOBoundaryAsSinglePart bool + readPartErrorPolicy ReadPartErrorPolicy + skipMalformedParts bool + rawContent bool + customParseMediaType CustomParseMediaType + stripMediaTypeInvalidCharacters bool + disableTextConversion bool + disableCharacterDetection bool + minCharsetDetectRunes int +} + +// defaultParser is a Parser with default configuration. +var defaultParser = *NewParser() + +// NewParser creates new parser with given options. +func NewParser(ops ...Option) *Parser { + // Construct parser with default options. + p := Parser{ + minCharsetDetectRunes: 100, + } + + for _, o := range ops { + o.apply(&p) + } + + return &p +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/part.go b/vendor/github.com/jhillyerd/enmime/v2/part.go new file mode 100644 index 000000000..ef39e5fe5 --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/part.go @@ -0,0 +1,505 @@ +package enmime + +import ( + "bufio" + "bytes" + "encoding/base64" + "io" + "math/rand" + "mime/quotedprintable" + "net/textproto" + "strconv" + "strings" + "time" + + "github.com/gogs/chardet" + "github.com/jhillyerd/enmime/v2/internal/coding" + inttp "github.com/jhillyerd/enmime/v2/internal/textproto" + "github.com/jhillyerd/enmime/v2/mediatype" + "github.com/pkg/errors" +) + +const ( + minCharsetConfidence = 85 +) + +// Part represents a node in the MIME multipart tree. The Content-Type, Disposition and File Name +// are parsed out of the header for easier access. +type Part struct { + PartID string // PartID labels this part's position within the tree. + Parent *Part // Parent of this part (can be nil.) + FirstChild *Part // FirstChild is the top most child of this part. + NextSibling *Part // NextSibling of this part. + Header textproto.MIMEHeader // Header for this part. + + Boundary string // Boundary marker used within this part. + ContentID string // ContentID header for cid URL scheme. + ContentType string // ContentType header without parameters. + ContentTypeParams map[string]string // Params, added to ContentType header. + Disposition string // Content-Disposition header without parameters. + FileName string // The file-name from disposition or type header. + FileModDate time.Time // The modification date of the file. + Charset string // The content charset encoding, may differ from charset in header. + OrigCharset string // The original content charset when a different charset was detected. + + Errors []*Error // Errors encountered while parsing this part. + Content []byte // Content after decoding, UTF-8 conversion if applicable. + ContentReader io.Reader // Reader interface for pulling the content for encoding. + Epilogue []byte // Epilogue contains data following the closing boundary marker. + + parser *Parser // Provides access to parsing options. + + randSource rand.Source // optional rand for uuid boundary generation + + encoder *Encoder // provides encoding options +} + +// NewPart creates a new Part object. +func NewPart(contentType string) *Part { + return &Part{ + Header: make(textproto.MIMEHeader), + ContentType: contentType, + ContentTypeParams: make(map[string]string), + parser: &defaultParser, + } +} + +// AddChild adds a child part to either FirstChild or the end of the children NextSibling chain. +// The child may have siblings and children attached. This method will set the Parent field on +// child and all its siblings. Safe to call on nil. +func (p *Part) AddChild(child *Part) { + if p == child { + // Prevent paradox. + return + } + if p != nil { + if p.FirstChild == nil { + // Make it the first child. + p.FirstChild = child + } else { + // Append to sibling chain. + current := p.FirstChild + for current.NextSibling != nil { + current = current.NextSibling + } + if current == child { + // Prevent infinite loop. + return + } + current.NextSibling = child + } + } + // Update all new first-level children Parent pointers. + for c := child; c != nil; c = c.NextSibling { + if c == c.NextSibling { + // Prevent infinite loop. + return + } + c.Parent = p + } +} + +// TextContent indicates whether the content is text based on its content type. This value +// determines what content transfer encoding scheme to use. +func (p *Part) TextContent() bool { + if p.ContentType == "" { + // RFC 2045: no CT is equivalent to "text/plain; charset=us-ascii" + return true + } + return strings.HasPrefix(p.ContentType, "text/") || + strings.HasPrefix(p.ContentType, ctMultipartPrefix) +} + +// setupHeaders reads the header, then populates the MIME header values for this Part. +func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error { + header, err := readHeader(r, p) + if err != nil { + return err + } + p.Header = textproto.MIMEHeader(header) + ctype := header.Get(hnContentType) + if ctype == "" { + if defaultContentType == "" { + p.addWarning(ErrorMissingContentType, "MIME parts should have a Content-Type header") + return nil + } + ctype = defaultContentType + } + // Parse Content-Type header. + mtype, mparams, minvalidParams, err := p.parseMediaType(ctype) + if err != nil { + return err + } + for i := range minvalidParams { + p.addWarningf( + ErrorMalformedHeader, + "Content-Type header has malformed parameter %q", + minvalidParams[i]) + } + p.ContentType = mtype + // Set disposition, filename, charset if available. + p.setupContentHeaders(mparams) + p.Boundary = mparams[hpBoundary] + p.ContentID = coding.FromIDHeader(header.Get(hnContentID)) + return nil +} + +// setupContentHeaders uses Content-Type media params and Content-Disposition headers to populate +// the disposition, filename, and charset fields. +func (p *Part) setupContentHeaders(mediaParams map[string]string) { + header := inttp.MIMEHeader(p.Header) + // Determine content disposition, filename, character set. + disposition, dparams, _, err := p.parseMediaType(header.Get(hnContentDisposition)) + if err == nil { + // Disposition is optional + p.Disposition = disposition + p.FileName = coding.DecodeExtHeader(dparams[hpFilename]) + } + if p.FileName == "" && mediaParams[hpName] != "" { + p.FileName = coding.DecodeExtHeader(mediaParams[hpName]) + } + if p.FileName == "" && mediaParams[hpFile] != "" { + p.FileName = coding.DecodeExtHeader(mediaParams[hpFile]) + } + if p.Charset == "" { + p.Charset = mediaParams[hpCharset] + } + if p.FileModDate.IsZero() { + p.FileModDate, _ = time.Parse(time.RFC822, mediaParams[hpModDate]) + } +} + +func (p *Part) readPartContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolicy) ([]byte, error) { + buf, err := io.ReadAll(r) + if err != nil { + if readPartErrorPolicy != nil && readPartErrorPolicy(p, err) { + p.addWarningf(ErrorMalformedChildPart, "partial content: %s", err.Error()) + return buf, nil + } + return nil, err + } + return buf, nil +} + +// convertFromDetectedCharset attempts to detect the character set for the given part, and returns +// an io.Reader that will convert from that charset to UTF-8. If the charset cannot be detected, +// this method adds a warning to the part and automatically falls back to using +// `convertFromStatedCharset` and returns the reader from that method. +func (p *Part) convertFromDetectedCharset(r io.Reader, readPartErrorPolicy ReadPartErrorPolicy) (io.Reader, error) { + // Attempt to detect character set from part content. + var cd *chardet.Detector + switch p.ContentType { + case "text/html": + cd = chardet.NewHtmlDetector() + default: + cd = chardet.NewTextDetector() + } + + buf, err := p.readPartContent(r, readPartErrorPolicy) + if err != nil { + return nil, errors.WithStack(err) + } + + // Restore r. + r = bytes.NewReader(buf) + + if p.parser.disableCharacterDetection && p.Charset != "" { + // Charset detection is disabled, use declared charset. + return p.convertFromStatedCharset(r), nil + } + + cs, err := cd.DetectBest(buf) + switch err { + case nil: + // Carry on + default: + p.addWarningf(ErrorCharsetDeclaration, "charset could not be detected: %v", err) + } + + if cs == nil || cs.Confidence < minCharsetConfidence || len(bytes.Runes(buf)) < p.parser.minCharsetDetectRunes { + // Low confidence or not enough characters, use declared character set. + return p.convertFromStatedCharset(r), nil + } + + // Confidence exceeded our threshold, use detected character set. + if p.Charset != "" && !strings.EqualFold(cs.Charset, p.Charset) { + p.addWarningf(ErrorCharsetDeclaration, + "declared charset %q, detected %q, confidence %d", + p.Charset, cs.Charset, cs.Confidence) + } + + if reader, err := coding.NewCharsetReader(cs.Charset, r); err == nil { + r = reader + p.OrigCharset = p.Charset + p.Charset = cs.Charset + } + + return r, nil +} + +// convertFromStatedCharset returns a reader that will convert from the charset specified for the +// current `*Part` to UTF-8. In case of error, or an unhandled character set, a warning will be +// added to the `*Part` and the original io.Reader will be returned. +func (p *Part) convertFromStatedCharset(r io.Reader) io.Reader { + if p.Charset == "" { + // US-ASCII. Just read. + return r + } + + reader, err := coding.NewCharsetReader(p.Charset, r) + if err != nil { + // Failed to get a conversion reader. + p.addWarningf(ErrorCharsetConversion, "failed to get reader for charset %q: %v", p.Charset, err) + } else { + return reader + } + + // Try to parse charset again here to see if we can salvage some badly formed + // ones like charset="charset=utf-8". + charsetp := strings.Split(p.Charset, "=") + if strings.EqualFold(charsetp[0], "charset") && len(charsetp) > 1 || + strings.EqualFold(charsetp[0], "iso") && len(charsetp) > 1 { + p.Charset = charsetp[1] + reader, err = coding.NewCharsetReader(p.Charset, r) + if err != nil { + // Failed to get a conversion reader. + p.addWarningf(ErrorCharsetConversion, "failed to get reader for charset %q: %v", p.Charset, err) + } else { + return reader + } + } + + return r +} + +// decodeContent performs transport decoding (base64, quoted-printable) and charset decoding, +// placing the result into Part.Content. IO errors will be returned immediately; other errors +// and warnings will be added to Part.Errors. +func (p *Part) decodeContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolicy) error { + header := inttp.MIMEHeader(p.Header) + // contentReader will point to the end of the content decoding pipeline. + contentReader := r + // b64cleaner aggregates errors, must maintain a reference to it to get them later. + var b64cleaner *coding.Base64Cleaner + // Build content decoding reader. + encoding := "" + if p.parser != nil && !p.parser.rawContent { + encoding = header.Get(hnContentEncoding) + } + validEncoding := true + switch strings.ToLower(encoding) { + case cteQuotedPrintable: + contentReader = coding.NewQPCleaner(contentReader) + contentReader = quotedprintable.NewReader(contentReader) + case cteBase64: + b64cleaner = coding.NewBase64Cleaner(contentReader) + contentReader = base64.NewDecoder(base64.RawStdEncoding, b64cleaner) + case cte8Bit, cte7Bit, cteBinary, "": + // No decoding required. + default: + // Unknown encoding. + validEncoding = false + p.addWarningf( + ErrorContentEncoding, + "Unrecognized Content-Transfer-Encoding type %q", + encoding) + } + // Build charset decoding reader. + if validEncoding && strings.HasPrefix(p.ContentType, "text/") && !p.parser.rawContent { + var err error + contentReader, err = p.convertFromDetectedCharset(contentReader, readPartErrorPolicy) + if err != nil { + return p.base64CorruptInputCheck(err) + } + } + // Decode and store content. + content, err := p.readPartContent(contentReader, readPartErrorPolicy) + if err != nil { + return p.base64CorruptInputCheck(errors.WithStack(err)) + } + p.Content = content + // Collect base64 errors. + if b64cleaner != nil { + for _, err := range b64cleaner.Errors { + p.addWarning(ErrorMalformedBase64, err.Error()) + } + } + // Set empty content-type error. + if p.ContentType == "" { + p.addWarningf( + ErrorMissingContentType, "content-type is empty for part id: %s", p.PartID) + } + return nil +} + +// parses media type using custom or default media type parser +func (p *Part) parseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) { + if p.parser == nil || p.parser.customParseMediaType == nil { + return mediatype.ParseWithOptions(ctype, mediatype.ParseOptions{StripMediaTypeInvalidCharacters: p.parser.stripMediaTypeInvalidCharacters}) + } + + return p.parser.customParseMediaType(ctype) +} + +// IsBase64CorruptInputError returns true when err is of type base64.CorruptInputError. +// +// It can be used to create ReadPartErrorPolicy functions. +func IsBase64CorruptInputError(err error) bool { + switch errors.Cause(err).(type) { + case base64.CorruptInputError: + return true + default: + return false + } +} + +// base64CorruptInputCheck will avoid fatal failure on corrupt base64 input +// +// This is a switch on errors.Cause(err).(type) for base64.CorruptInputError +func (p *Part) base64CorruptInputCheck(err error) error { + if IsBase64CorruptInputError(err) { + p.Content = nil + p.addError(ErrorMalformedBase64, err.Error()) + return nil + } + return err +} + +// Clone returns a clone of the current Part. +func (p *Part) Clone(parent *Part) *Part { + if p == nil { + return nil + } + + newPart := &Part{ + PartID: p.PartID, + Header: p.Header, + Parent: parent, + Boundary: p.Boundary, + ContentID: p.ContentID, + ContentType: p.ContentType, + Disposition: p.Disposition, + FileName: p.FileName, + Charset: p.Charset, + Errors: p.Errors, + Content: p.Content, + Epilogue: p.Epilogue, + } + newPart.FirstChild = p.FirstChild.Clone(newPart) + newPart.NextSibling = p.NextSibling.Clone(parent) + + return newPart +} + +// ReadParts reads a MIME document from the provided reader and parses it into tree of Part objects. +func ReadParts(r io.Reader) (*Part, error) { + return defaultParser.ReadParts(r) +} + +// ReadParts reads a MIME document from the provided reader and parses it into tree of Part objects. +func (p Parser) ReadParts(r io.Reader) (*Part, error) { + br := bufio.NewReader(r) + root := &Part{PartID: "0", parser: &p} + + // Read header; top-level default CT is text/plain us-ascii according to RFC 822. + if err := root.setupHeaders(br, `text/plain; charset="us-ascii"`); err != nil { + return nil, err + } + + if detectMultipartMessage(root, p.multipartWOBoundaryAsSinglePart) { + // Content is multipart, parse it. + if err := parseParts(root, br); err != nil { + return nil, err + } + } else { + // Content is text or data, decode it. + if err := root.decodeContent(br, p.readPartErrorPolicy); err != nil { + return nil, err + } + } + return root, nil +} + +// parseParts recursively parses a MIME multipart document and sets each Parts PartID. +func parseParts(parent *Part, reader *bufio.Reader) error { + firstRecursion := parent.Parent == nil + // Loop over MIME boundaries. + br := newBoundaryReader(reader, parent.Boundary) + for indexPartID := 1; true; indexPartID++ { + next, err := br.Next() + if err != nil && errors.Cause(err) != io.EOF { + return err + } + if br.unbounded { + parent.addWarningf(ErrorMissingBoundary, "Boundary %q was not closed correctly", + parent.Boundary) + } + if !next { + break + } + + // Set this Part's PartID, indicating its position within the MIME Part tree. + p := &Part{parser: parent.parser} + if firstRecursion { + p.PartID = strconv.Itoa(indexPartID) + } else { + p.PartID = parent.PartID + "." + strconv.Itoa(indexPartID) + } + + // Look for part header. + bbr := bufio.NewReader(br) + if err = p.setupHeaders(bbr, ""); err != nil { + if p.parser.skipMalformedParts { + parent.addErrorf(ErrorMalformedChildPart, "read header: %s", err.Error()) + continue + } + + return err + } + + // Insert this Part into the MIME tree. + if p.Boundary == "" { + // Content is text or data, decode it. + if err = p.decodeContent(bbr, p.parser.readPartErrorPolicy); err != nil { + if p.parser.skipMalformedParts { + parent.addErrorf(ErrorMalformedChildPart, "decode content: %s", err.Error()) + continue + } + return err + } + parent.AddChild(p) + continue + } + + parent.AddChild(p) + // Content is another multipart. + if err = parseParts(p, bbr); err != nil { + if p.parser.skipMalformedParts { + parent.addErrorf(ErrorMalformedChildPart, "parse parts: %s", err.Error()) + continue + } + return err + } + } + + // Store any content following the closing boundary marker into the epilogue. + epilogue, err := io.ReadAll(reader) + if err != nil { + return errors.WithStack(err) + } + parent.Epilogue = epilogue + + // If a Part is "multipart/" Content-Type, it will have .0 appended to its PartID + // i.e. it is the root of its MIME Part subtree. + if !firstRecursion { + parent.PartID += ".0" + } + return nil +} + +func (p *Part) WithEncoder(e *Encoder) *Part { + if e != nil { + p.encoder = e + } + return p +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/sender.go b/vendor/github.com/jhillyerd/enmime/v2/sender.go new file mode 100644 index 000000000..10885717d --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/sender.go @@ -0,0 +1,33 @@ +package enmime + +import "net/smtp" + +// Sender provides a method for enmime to send an email. +type Sender interface { + // Sends the provided msg to the specified recipients, providing the specified reverse-path to + // the mail server to use for delivery error reporting. + // + // The message headers should usually include fields such as "From", "To", "Subject", and "Cc". + // Sending "Bcc" messages is accomplished by including an email address in the recipients + // parameter but not including it in the message headers. + Send(reversePath string, recipients []string, msg []byte) error +} + +// SMTPSender is a Sender backed by Go's built-in net/smtp.SendMail function. +type SMTPSender struct { + addr string + auth smtp.Auth +} + +var _ Sender = &SMTPSender{} + +// NewSMTP creates a new SMTPSender, which uses net/smtp.SendMail, and accepts the same +// authentication parameters. If no authentication is required, `auth` may be nil. +func NewSMTP(addr string, auth smtp.Auth) *SMTPSender { + return &SMTPSender{addr, auth} +} + +// Send a message using net/smtp.SendMail. +func (s *SMTPSender) Send(reversePath string, recipients []string, msg []byte) error { + return smtp.SendMail(s.addr, s.auth, reversePath, recipients, msg) +} diff --git a/vendor/github.com/jhillyerd/enmime/v2/shell.nix b/vendor/github.com/jhillyerd/enmime/v2/shell.nix new file mode 100644 index 000000000..11538219e --- /dev/null +++ b/vendor/github.com/jhillyerd/enmime/v2/shell.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } }: +pkgs.mkShell { + buildInputs = with pkgs; [ + delve + go + golangci-lint + gopls + ]; + + hardeningDisable = [ "fortify" ]; +} diff --git a/vendor/github.com/ssor/bom/.travis.yml b/vendor/github.com/ssor/bom/.travis.yml new file mode 100644 index 000000000..6c7f48efd --- /dev/null +++ b/vendor/github.com/ssor/bom/.travis.yml @@ -0,0 +1,14 @@ +language: go +go: + - tip + - 1.8 + - 1.7 + - 1.6 + - 1.5 + - 1.4 + - 1.3 + - 1.2 +notifications: + email: + on_success: change + on_failure: always diff --git a/vendor/github.com/ssor/bom/LICENSE b/vendor/github.com/ssor/bom/LICENSE new file mode 100644 index 000000000..374f6855a --- /dev/null +++ b/vendor/github.com/ssor/bom/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Asher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ssor/bom/README.md b/vendor/github.com/ssor/bom/README.md new file mode 100644 index 000000000..2dcc289ff --- /dev/null +++ b/vendor/github.com/ssor/bom/README.md @@ -0,0 +1,23 @@ +# bom +small tools for cleaning bom from byte array or reader + + +## Installation + +```sh +$ go get github.com/ssor/bom +``` + +## How to Use + + +``` + bs := []byte{bom0, bom1, bom2, 0x11} + result := CleanBom(bs) +``` + +``` + bs := []byte{bom0, bom1, bom2, 0x11} + result := NewReaderWithoutBom(bytes.NewReader(bs)) + +``` \ No newline at end of file diff --git a/vendor/github.com/ssor/bom/bom.go b/vendor/github.com/ssor/bom/bom.go new file mode 100644 index 000000000..907ea98d6 --- /dev/null +++ b/vendor/github.com/ssor/bom/bom.go @@ -0,0 +1,34 @@ +package bom + +import ( + "bytes" + "io" + "io/ioutil" +) + +const ( + bom0 = 0xef + bom1 = 0xbb + bom2 = 0xbf +) + +// CleanBom returns b with the 3 byte BOM stripped off the front if it is present. +// If the BOM is not present, then b is returned. +func CleanBom(b []byte) []byte { + if len(b) >= 3 && + b[0] == bom0 && + b[1] == bom1 && + b[2] == bom2 { + return b[3:] + } + return b +} + +// NewReaderWithoutBom returns an io.Reader that will skip over initial UTF-8 byte order marks. +func NewReaderWithoutBom(r io.Reader) (io.Reader, error) { + bs, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + return bytes.NewReader(CleanBom(bs)), nil +} diff --git a/vendor/gopkg.in/loremipsum.v1/.editorconfig b/vendor/gopkg.in/loremipsum.v1/.editorconfig new file mode 100644 index 000000000..c95297f07 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = false +trim_trailing_whitespace = true +charset = utf-8 + +[*.go] +indent_style = tab +indent_size = 4 + +[Makefile] +indent_style = tab +indent_size = 4 diff --git a/vendor/gopkg.in/loremipsum.v1/.gitignore b/vendor/gopkg.in/loremipsum.v1/.gitignore new file mode 100644 index 000000000..f1c181ec9 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/.gitignore @@ -0,0 +1,12 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/vendor/gopkg.in/loremipsum.v1/.travis.yml b/vendor/gopkg.in/loremipsum.v1/.travis.yml new file mode 100644 index 000000000..f5483dba0 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/.travis.yml @@ -0,0 +1,28 @@ +language: go + +go: 1.13.x + +matrix: + include: + - go: 1.13.x + env: COVERAGE=true + - go: 1.12.x + env: COVERAGE=false + - go: 1.11.x + env: COVERAGE=false + - go: 1.10.x + env: COVERAGE=false + - go: 1.9.x + env: COVERAGE=false + +install: + - go get -t ./... + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls + +script: + - ./travis.sh + +env: + global: + secure: "AHr7rzXYWGTIe8CvDLtEIwj3Arig7u9tQt1mWbB6E4VpTHOu1AeHMn/NXPm5aJV+eNfmRuiYQ4E2jswhHxcxp8M7fdqmNjOv8hGOuc1cyT2yKzD087qXHbreO5tfp/BMilFxzUe/tJRsSyj5RHNQ5krIQpnZMoqAX0IgtoPL5VI8w+xOM8PatUIGK9rVoYj3HXgUNrTGWPe6pCAKtKG3zZFnQlRZxfx+WAzMTu/oWnX9/HJEKHL9c5O8xHQvU2qlHVKoHpVVtZ1wchTrUJMjZotN1veZY40hJ+pStBrNmZCWRvaigAVaTIT8KERZGv83I4DIHQruDF/de5ecBEATSQ14KMvXLR4PGDPWeA8itlcspc0d2RiBTS8ClGhowlo5vxQ4rIim0Qy/+zMOeohGMgz8jqLer1Pt9hB5XKnZcigP1mAakDBNL45lXZxGC/w3SAI5+DHkkJmMaDbExhybkWCDgcWx+pNwoxN6/scWf2MMnZ361fRGPAc8namUxyhgwlp1DbJPcMduEq8XD8IUFMUCkRnz/8BAJx30aLWlj5eQ2GbjlNli3H3wV6RcXNzfuHBLIltITJjvrENsgeWOJ9skrVUx6EFzyBtHeKQEWaRiS4NJwttuvg7uAAB7Tf0NJVVeV3g7EJuoN1K244sGa4XQ4YxCXnQMLLcWVWL5SJc=" diff --git a/vendor/gopkg.in/loremipsum.v1/LICENSE b/vendor/gopkg.in/loremipsum.v1/LICENSE new file mode 100644 index 000000000..d4e6ae934 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 go-loremipsum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/gopkg.in/loremipsum.v1/Makefile b/vendor/gopkg.in/loremipsum.v1/Makefile new file mode 100644 index 000000000..80aeb2192 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/Makefile @@ -0,0 +1,3 @@ +test: + go vet ./... + go test -cover ./... diff --git a/vendor/gopkg.in/loremipsum.v1/README.md b/vendor/gopkg.in/loremipsum.v1/README.md new file mode 100644 index 000000000..b0c945864 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/README.md @@ -0,0 +1,91 @@ +# A lorem ipsum generator for Go + +[![Build Status](https://travis-ci.org/go-loremipsum/loremipsum.svg?branch=master)](https://travis-ci.org/go-loremipsum/loremipsum) +[![Coverage Status](https://coveralls.io/repos/github/go-loremipsum/loremipsum/badge.svg?branch=master)](https://coveralls.io/github/go-loremipsum/loremipsum?branch=master) +[![Go Report Card](https://goreportcard.com/badge/gopkg.in/loremipsum.v1)](https://goreportcard.com/report/gopkg.in/loremipsum.v1) +[![GoDoc](https://godoc.org/gopkg.in/loremipsum.v1?status.svg)](https://godoc.org/gopkg.in/loremipsum.v1) + +## Usage + +~~~go +import "gopkg.in/loremipsum.v1" + +loremIpsumGenerator := loremipsum.New() +~~~ + +Or, if you want repeatable output, pass seed + +~~~go +import "gopkg.in/loremipsum.v1" + +loremIpsumGenerator := loremipsum.NewWithSeed(1234) +~~~ + + +### Generate random lorem ipsum word + +~~~go +word := loremIpsumGenerator.Word() +fmt.Println(word) +~~~ + +~~~ +interdum +~~~ + +### Generate random lorem ipsum words + +~~~go +words := loremIpsumGenerator.Words(5) +fmt.Println(words) +~~~ + +~~~ +lorem ipsum dolor sit amet +~~~ + +### Generate random lorem ipsum sentence + +~~~go +sentence := loremIpsumGenerator.Sentence() +fmt.Println(sentence) +~~~ + +~~~ +Lorem ipsum dolor sit amet consectetur adipiscing elit tincidunt inceptos quis faucibus nunc maecenas nostra eros nam mollis augue habitasse mattis per enim odio suscipit. +~~~ + +### Generate random lorem ipsum sentences + +~~~go +sentences := loremIpsumGenerator.Sentences(2) +fmt.Println(sentences) +~~~ + +~~~ +Lorem ipsum dolor sit amet consectetur adipiscing elit id sem facilisi sapien nibh curabitur nam viverra lacinia. Luctus conubia pulvinar ornare natoque hendrerit dui praesent libero porttitor at suspendisse amet viverra nisl tristique hac ad eget semper et ligula vulputate. +~~~ + +### Generate random lorem ipsum paragraph + +~~~go +paragraph := loremIpsumGenerator.Paragraph() +fmt.Println(paragraph) +~~~ + +~~~ +Lorem ipsum dolor sit amet consectetur adipiscing elit cubilia lobortis efficitur nunc diam egestas cursus laoreet interdum integer rutrum lacus elementum venenatis. Tincidunt feugiat nam hendrerit bibendum suspendisse interdum rhoncus, diam inceptos auctor nisi non vivamus dictum platea tristique euismod egestas odio lacinia. Fusce placerat dolor dui massa venenatis bibendum auctor magnis nisl molestie euismod ipsum rutrum neque lorem nisi justo odio a. Lectus pretium sem a mi eleifend interdum mus porta rutrum suspendisse quis cubilia habitasse luctus dolor metus aenean. Imperdiet amet magna etiam vulputate sollicitudin facilisis mollis duis taciti tristique magnis fusce porttitor. At scelerisque sapien amet venenatis a finibus neque quam dictum natoque tempus ridiculus porttitor ultricies diam luctus dis enim. +~~~ + +### Generate random lorem ipsum paragraphs + +~~~go +paragraphs := loremIpsumGenerator.Paragraphs(3) +fmt.Println(paragraphs) +~~~ + +~~~ +Lorem ipsum dolor sit amet consectetur adipiscing elit habitasse nascetur arcu orci nisl torquent rutrum aenean nisi primis felis fusce dui. Suscipit hac montes neque duis dignissim sem pharetra sit laoreet eu curabitur vivamus class eleifend. Sagittis porta suspendisse felis turpis vehicula ad habitant dignissim pulvinar himenaeos at consectetur morbi luctus faucibus ultricies euismod volutpat maecenas. Sollicitudin facilisis ligula platea litora maecenas molestie rhoncus fermentum velit porta eu dictumst laoreet donec class potenti etiam bibendum sagittis inceptos dapibus magna pharetra porttitor suscipit aptent convallis nulla lacinia eget. +Fames libero id nunc eu malesuada nisl feugiat quam purus enim quisque porttitor velit dolor augue etiam tempor dictumst neque mattis conubia facilisis ullamcorper scelerisque natoque lacus fusce proin pharetra magnis rhoncus. Ante condimentum mi vel odio class nullam nostra nam taciti vitae nec potenti maecenas sit sodales ligula tincidunt montes pretium eu. Ornare euismod mollis ex augue lacus aliquam habitant mi donec sollicitudin consequat rutrum finibus fames lobortis bibendum leo ullamcorper gravida ac turpis ultricies convallis dictumst erat amet. Nostra lorem semper nisi fringilla ac integer odio dolor fusce sociosqu sollicitudin habitasse lacinia mauris blandit montes imperdiet nunc urna. Aliquam semper rutrum amet nam cursus donec turpis ut interdum convallis felis finibus luctus risus posuere. +Enim massa hendrerit fames faucibus tempor porta mi laoreet habitasse ligula purus rutrum facilisis interdum donec varius fringilla nibh nam eleifend. Lacinia tempor augue quis ut tortor eleifend varius facilisis sagittis pharetra, feugiat habitant magna porttitor iaculis nisl pellentesque egestas maximus praesent habitasse congue nostra elementum luctus nam potenti euismod etiam torquent class. Hendrerit euismod cras egestas tempus congue parturient, ultrices finibus dictumst pharetra eleifend donec elementum sollicitudin lobortis magna nascetur dolor curabitur. Eros ac feugiat ridiculus fringilla fusce adipiscing, massa ad est ornare vitae facilisis parturient molestie leo mauris rutrum lectus a. +~~~ diff --git a/vendor/gopkg.in/loremipsum.v1/doc.go b/vendor/gopkg.in/loremipsum.v1/doc.go new file mode 100644 index 000000000..5e4d3a699 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/doc.go @@ -0,0 +1,2 @@ +// Package loremipsum provides lorem ipsum strings generator. +package loremipsum diff --git a/vendor/gopkg.in/loremipsum.v1/go110.go b/vendor/gopkg.in/loremipsum.v1/go110.go new file mode 100644 index 000000000..ab657a1d2 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/go110.go @@ -0,0 +1,29 @@ +//go:build go1.10 +// +build go1.10 + +package loremipsum + +// Shuffle the words +func (li *LoremIpsum) shuffle() { + var words []string + + if !li.first { + words = make([]string, len(loremIpsumWords)) + copy(words, loremIpsumWords[:]) + } else { + words = make([]string, len(rest)) + copy(words, rest) + } + li.rng.Shuffle(len(words), func(i int, j int) { + words[i], words[j] = words[j], words[i] + }) + if li.first { + b := make([]string, len(beg)) + copy(b, beg) + // words, b = b, words + // words = append(words, b...) + words = append(b, words...) + } + li.words = words + li.first = false +} diff --git a/vendor/gopkg.in/loremipsum.v1/go19.go b/vendor/gopkg.in/loremipsum.v1/go19.go new file mode 100644 index 000000000..8622017a8 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/go19.go @@ -0,0 +1,79 @@ +//go:build !go1.10 +// +build !go1.10 + +package loremipsum + +import "math/rand" + +// int31n returns, as an int32, a non-negative pseudo-random number in [0,n). +// n must be > 0, but int31n does not check this; the caller must ensure it. +// int31n exists because Int31n is inefficient, but Go 1 compatibility +// requires that the stream of values produced by math/rand remain unchanged. +// int31n can thus only be used internally, by newly introduced APIs. +// +// For implementation details, see: +// http://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction +// http://lemire.me/blog/2016/06/30/fast-random-shuffling +func (li *LoremIpsum) int31n(n int32) int32 { + v := li.rng.Uint32() + prod := uint64(v) * uint64(n) + low := uint32(prod) + if low < uint32(n) { + thresh := uint32(-n) % uint32(n) + for low < thresh { + v = li.rng.Uint32() + prod = uint64(v) * uint64(n) + low = uint32(prod) + } + } + return int32(prod >> 32) +} + +// Shuffle pseudo-randomizes the order of elements. +// n is the number of elements. Shuffle panics if n < 0. +// swap swaps the elements with indexes i and j. +func (li *LoremIpsum) shuffleWords(n int, swap func(i, j int)) { + if n < 0 { + panic("invalid argument to Shuffle") + } + // Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + // Shuffle really ought not be called with n that doesn't fit in 32 bits. + // Not only will it take a very long time, but with 2³¹! possible permutations, + // there's no way that any PRNG can have a big enough internal state to + // generate even a minuscule percentage of the possible permutations. + // Nevertheless, the right API signature accepts an int n, so handle it as best we can. + i := n - 1 + for ; i > 1<<31-1-1; i-- { + j := int(rand.Int63n(int64(i + 1))) + swap(i, j) + } + for ; i > 0; i-- { + j := int(li.int31n(int32(i + 1))) + swap(i, j) + } +} + +// Shuffle the words +func (li *LoremIpsum) shuffle() { + var words []string + + if !li.first { + words = make([]string, len(loremIpsumWords)) + copy(words, loremIpsumWords[:]) + } else { + words = make([]string, len(rest)) + copy(words, rest) + } + li.shuffleWords(len(words), func(i int, j int) { + words[i], words[j] = words[j], words[i] + }) + if li.first { + b := make([]string, len(beg)) + copy(b, beg) + // words, b = b, words + // words = append(words, b...) + words = append(b, words...) + } + li.words = words + li.first = false +} diff --git a/vendor/gopkg.in/loremipsum.v1/loremipsum.go b/vendor/gopkg.in/loremipsum.v1/loremipsum.go new file mode 100644 index 000000000..fc9620dd9 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/loremipsum.go @@ -0,0 +1,103 @@ +package loremipsum + +import ( + "math/rand" + "strings" + "time" +) + +// LoremIpsum is a lorem ipsum generator +type LoremIpsum struct { + first bool + words []string + idx int + rng *rand.Rand +} + +// New returns new instance of LoremIpsum +func New() *LoremIpsum { + return NewWithSeed(time.Now().Unix()) +} + +// New returns new instance of LoremIpsum with PRNG seeded with the parameter +func NewWithSeed(seed int64) *LoremIpsum { + return NewWithSource(rand.NewSource(seed)) +} + +// NewWithSource returns new instance of LoremIpsum that uses random values +// from source to generate words. +func NewWithSource(source rand.Source) *LoremIpsum { + li := new(LoremIpsum) + li.rng = rand.New(source) + li.first = true + li.idx = 0 + li.shuffle() + return li +} + +// Word returns a single word of lorem ipsum +func (li *LoremIpsum) Word() string { + return li.words[li.rng.Intn(len(li.words))] +} + +// WordList returns list of words of lorem ipsum +func (li *LoremIpsum) WordList(count int) []string { + return li.words[:count] +} + +// Words returns words of lorem ipsum +func (li *LoremIpsum) Words(count int) string { + return strings.Join(li.WordList(count), " ") +} + +// Sentence returns full sentence of lorem ipsum +func (li *LoremIpsum) Sentence() string { + for { + l := int(li.gauss(24.46, 5.08)) + if l > 0 { + words := li.words[:l] + return li.punctuate(words) + } + } +} + +// SentenceList returns list of sentences of lorem ipsum +func (li *LoremIpsum) SentenceList(count int) []string { + var sentences []string + sentences = make([]string, count) + for idx := range sentences { + sentences[idx] = li.Sentence() + li.shuffle() + } + return sentences +} + +// Sentences returns sentences of lorem ipsum +func (li *LoremIpsum) Sentences(count int) string { + return strings.Join(li.SentenceList(count), " ") +} + +// Paragraph returns full paragraph of lorem ipsum +func (li *LoremIpsum) Paragraph() string { + for { + count := int(li.gauss(5.8, 1.93)) + if count > 0 { + return li.Sentences(count) + } + } +} + +// ParagraphList returns list of paragraphs of lorem ipsum +func (li *LoremIpsum) ParagraphList(count int) []string { + var paragraphs []string + paragraphs = make([]string, count) + for idx := range paragraphs { + paragraphs[idx] = li.Paragraph() + } + return paragraphs +} + +// Paragraphs returns paragraphs of lorem ipsum +func (li *LoremIpsum) Paragraphs(count int) string { + return strings.Join(li.ParagraphList(count), "\n") +} diff --git a/vendor/gopkg.in/loremipsum.v1/travis.sh b/vendor/gopkg.in/loremipsum.v1/travis.sh new file mode 100644 index 000000000..8a5f3681d --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/travis.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -ev +if [ "${COVERAGE}" = "true" ]; then + go test -v -covermode=count -coverprofile=coverage.out ./... + goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN +else + go test -v ./... +fi diff --git a/vendor/gopkg.in/loremipsum.v1/util.go b/vendor/gopkg.in/loremipsum.v1/util.go new file mode 100644 index 000000000..b7c15eb87 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/util.go @@ -0,0 +1,37 @@ +package loremipsum + +import ( + "math" + "strings" +) + +func (li *LoremIpsum) gauss(mean, stdDev float64) float64 { + x := li.rng.Float64() + y := li.rng.Float64() + z := math.Sqrt(-2*math.Log(x)) * math.Cos(2*math.Pi*y) + return z*stdDev + mean +} + +func (li *LoremIpsum) punctuate(sentence []string) string { + count := len(sentence) + if count > 4 { + mean := math.Log(float64(count)) / math.Log(6.0) + stdDev := mean / 6 + commas := int(li.gauss(mean, stdDev)) + for i := 1; i < commas; i++ { + idx := int(float64(i) * float64(count) / (float64(commas) + 1)) + if idx > 0 && idx < (count-1) { + sentence[idx] = sentence[idx] + "," + } + } + } + + first := strings.Split(sentence[0], "") + first[0] = strings.ToUpper(first[0]) + sentence[0] = strings.Join(first, "") + + lastIdx := count - 1 + sentence[lastIdx] = sentence[lastIdx] + "." + + return strings.Join(sentence, " ") +} diff --git a/vendor/gopkg.in/loremipsum.v1/words.go b/vendor/gopkg.in/loremipsum.v1/words.go new file mode 100644 index 000000000..0227fe5b0 --- /dev/null +++ b/vendor/gopkg.in/loremipsum.v1/words.go @@ -0,0 +1,35 @@ +package loremipsum + +var beg = loremIpsumWords[:8] +var rest = loremIpsumWords[8:] + +// LoremIpsumWords contains all Lorem Ipsum words +var loremIpsumWords = [...]string{"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", + "a", "ac", "accumsan", "ad", "aenean", "aliquam", "aliquet", "ante", + "aptent", "arcu", "at", "auctor", "augue", "bibendum", "blandit", + "class", "commodo", "condimentum", "congue", "consequat", "conubia", + "convallis", "cras", "cubilia", "curabitur", "curae", "cursus", + "dapibus", "diam", "dictum", "dictumst", "dignissim", "dis", "donec", + "dui", "duis", "efficitur", "egestas", "eget", "eleifend", "elementum", + "enim", "erat", "eros", "est", "et", "etiam", "eu", "euismod", "ex", + "facilisi", "facilisis", "fames", "faucibus", "felis", "fermentum", + "feugiat", "finibus", "fringilla", "fusce", "gravida", "habitant", + "habitasse", "hac", "hendrerit", "himenaeos", "iaculis", "id", + "imperdiet", "in", "inceptos", "integer", "interdum", "justo", + "lacinia", "lacus", "laoreet", "lectus", "leo", "libero", "ligula", + "litora", "lobortis", "luctus", "maecenas", "magna", "magnis", + "malesuada", "massa", "mattis", "mauris", "maximus", "metus", "mi", + "molestie", "mollis", "montes", "morbi", "mus", "nam", "nascetur", + "natoque", "nec", "neque", "netus", "nibh", "nisi", "nisl", "non", + "nostra", "nulla", "nullam", "nunc", "odio", "orci", "ornare", + "parturient", "pellentesque", "penatibus", "per", "pharetra", + "phasellus", "placerat", "platea", "porta", "porttitor", "posuere", + "potenti", "praesent", "pretium", "primis", "proin", "pulvinar", + "purus", "quam", "quis", "quisque", "rhoncus", "ridiculus", "risus", + "rutrum", "sagittis", "sapien", "scelerisque", "sed", "sem", "semper", + "senectus", "sociosqu", "sodales", "sollicitudin", "suscipit", + "suspendisse", "taciti", "tellus", "tempor", "tempus", "tincidunt", + "torquent", "tortor", "tristique", "turpis", "ullamcorper", "ultrices", + "ultricies", "urna", "ut", "varius", "vehicula", "vel", "velit", + "venenatis", "vestibulum", "vitae", "vivamus", "viverra", "volutpat", + "vulputate"} diff --git a/vendor/modules.txt b/vendor/modules.txt index 41612346f..62feb87cf 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -243,6 +243,9 @@ github.com/cenkalti/backoff/v4 github.com/cenkalti/backoff/v5 # github.com/ceph/go-ceph v0.37.0 ## explicit; go 1.24.0 +# github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a +## explicit +github.com/cention-sany/utf7 github.com/ceph/go-ceph/cephfs github.com/ceph/go-ceph/cephfs/admin github.com/ceph/go-ceph/common/admin/manager @@ -423,6 +426,9 @@ github.com/docker/go-units # github.com/dustin/go-humanize v1.0.1 ## explicit; go 1.16 github.com/dustin/go-humanize +# github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 +## explicit; go 1.20 +github.com/dustinkirkland/golang-petname # github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e ## explicit github.com/dutchcoders/go-clamd @@ -435,6 +441,22 @@ github.com/ebitengine/purego/internal/strings # github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc ## explicit github.com/egirna/icap +# github.com/emersion/go-imap/v2 v2.0.0-beta.5 +## explicit; go 1.18 +github.com/emersion/go-imap/v2 +github.com/emersion/go-imap/v2/imapclient +github.com/emersion/go-imap/v2/internal +github.com/emersion/go-imap/v2/internal/imapnum +github.com/emersion/go-imap/v2/internal/imapwire +github.com/emersion/go-imap/v2/internal/utf7 +# github.com/emersion/go-message v0.18.1 +## explicit; go 1.14 +github.com/emersion/go-message +github.com/emersion/go-message/mail +github.com/emersion/go-message/textproto +# github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 +## explicit; go 1.12 +github.com/emersion/go-sasl # github.com/emirpasic/gods v1.18.1 ## explicit; go 1.2 github.com/emirpasic/gods/containers @@ -488,6 +510,16 @@ github.com/go-chi/chi/v5/middleware # github.com/go-chi/render v1.0.3 ## explicit; go 1.16 github.com/go-chi/render +# github.com/go-crypt/crypt v0.4.5 +## explicit; go 1.23 +github.com/go-crypt/crypt/algorithm +github.com/go-crypt/crypt/algorithm/shacrypt +github.com/go-crypt/crypt/internal/encoding +github.com/go-crypt/crypt/internal/random +# github.com/go-crypt/x v0.4.7 +## explicit; go 1.23.0 +github.com/go-crypt/x/base64 +github.com/go-crypt/x/crypt # github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 ## explicit; go 1.13 github.com/go-git/gcfg @@ -656,7 +688,7 @@ github.com/go-task/slim-sprig # github.com/go-task/slim-sprig/v3 v3.0.0 ## explicit; go 1.20 github.com/go-task/slim-sprig/v3 -# github.com/go-test/deep v1.1.0 +# github.com/go-test/deep v1.1.1 ## explicit; go 1.16 # github.com/go-viper/mapstructure/v2 v2.4.0 ## explicit; go 1.18 @@ -718,6 +750,9 @@ github.com/gofrs/uuid github.com/gogo/protobuf/gogoproto github.com/gogo/protobuf/proto github.com/gogo/protobuf/protoc-gen-gogo/descriptor +# github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f +## explicit +github.com/gogs/chardet # github.com/golang-jwt/jwt/v4 v4.5.2 ## explicit; go 1.16 github.com/golang-jwt/jwt/v4 @@ -845,6 +880,9 @@ github.com/iancoleman/strcase # github.com/imdario/mergo v0.3.15 ## explicit; go 1.13 github.com/imdario/mergo +# github.com/inbucket/html2text v0.9.0 +## explicit; go 1.23.9 +github.com/inbucket/html2text # github.com/inconshreveable/mousetrap v1.1.0 ## explicit; go 1.18 github.com/inconshreveable/mousetrap @@ -861,6 +899,13 @@ github.com/jellydator/ttlcache/v2 # github.com/jellydator/ttlcache/v3 v3.4.0 ## explicit; go 1.23.0 github.com/jellydator/ttlcache/v3 +# github.com/jhillyerd/enmime/v2 v2.2.0 +## explicit; go 1.23.9 +github.com/jhillyerd/enmime/v2 +github.com/jhillyerd/enmime/v2/internal/coding +github.com/jhillyerd/enmime/v2/internal/stringutil +github.com/jhillyerd/enmime/v2/internal/textproto +github.com/jhillyerd/enmime/v2/mediatype # github.com/jinzhu/now v1.1.5 ## explicit; go 1.12 github.com/jinzhu/now @@ -2080,6 +2125,9 @@ github.com/spf13/viper/internal/encoding/json github.com/spf13/viper/internal/encoding/toml github.com/spf13/viper/internal/encoding/yaml github.com/spf13/viper/internal/features +# github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf +## explicit +github.com/ssor/bom # github.com/stretchr/objx v0.5.2 ## explicit; go 1.20 github.com/stretchr/objx @@ -2728,6 +2776,9 @@ google.golang.org/protobuf/types/known/wrapperspb # gopkg.in/cenkalti/backoff.v1 v1.1.0 ## explicit gopkg.in/cenkalti/backoff.v1 +# gopkg.in/loremipsum.v1 v1.1.2 +## explicit; go 1.17 +gopkg.in/loremipsum.v1 # gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 ## explicit gopkg.in/tomb.v1