mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-07 04:40:05 -06:00
test(groupware): add testcontainers based jmap test
* adds pkg/jmap/jmap_integration_test.go
* uses ghcr.io/stalwartlabs/stalwart:v0.13.2-alpine
* can be disabled by setting one of the following environment
variables, in the same fashion as ca0493b28
- CI=woodpecker
- CI_SYSTEM_NAME=woodpecker
- USE_TESTCONTAINERS=false
* dependencies:
- bump github.com/go-test/deep from 1.1.0 to 1.1.1
- add github.com/cention-sany/utf7
- add github.com/dustinkirkland/golang-petname
- add github.com/emersion/go-imap/v2
- add github.com/emersion/go-message
- add github.com/emersion/go-sasl
- add github.com/go-crypt/crypt
- add github.com/go-crypt/x
- add github.com/gogs/chardet
- add github.com/inbucket/html2text
- add github.com/jhilleryerd/enmime/v2
- add github.com/ssor/bom
- add gopkg.in/loremipsum.v1
This commit is contained in:
14
go.mod
14
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.11
|
||||
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.3
|
||||
github.com/go-chi/render v1.0.3
|
||||
@@ -164,6 +165,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.36.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.3.1 // indirect
|
||||
@@ -195,8 +197,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
|
||||
@@ -206,6 +211,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
|
||||
@@ -226,7 +233,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
|
||||
@@ -237,6 +244,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-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
@@ -255,8 +263,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
|
||||
@@ -354,6 +364,7 @@ require (
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/spacewander/go-suffix-tree v0.0.0-20191010040751-0865e368c784 // 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/tchap/go-patricia/v2 v2.3.3 // indirect
|
||||
@@ -393,6 +404,7 @@ require (
|
||||
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // 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
|
||||
|
||||
25
go.sum
25
go.sum
@@ -216,6 +216,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.36.0 h1:IDE4vEF+4fmjve+CPjD1WStgfQ+Lh6vD+9PMUI712KI=
|
||||
github.com/ceph/go-ceph v0.36.0/go.mod h1:fGCbndVDLuHW7q2954d6y+tgPFOBnRLqJRe2YXyngw4=
|
||||
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=
|
||||
@@ -322,6 +324,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=
|
||||
@@ -333,6 +337,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=
|
||||
@@ -388,6 +398,10 @@ github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hH
|
||||
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=
|
||||
@@ -475,6 +489,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=
|
||||
@@ -503,6 +518,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=
|
||||
@@ -668,6 +685,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=
|
||||
@@ -694,6 +713,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=
|
||||
@@ -1167,6 +1188,8 @@ 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/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=
|
||||
@@ -1788,6 +1811,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=
|
||||
|
||||
466
pkg/jmap/jmap_integration_test.go
Normal file
466
pkg/jmap/jmap_integration_test.go
Normal file
@@ -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("<p>%v</p>", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
vendor/github.com/MicahParks/jwkset/storage.go
generated
vendored
1
vendor/github.com/MicahParks/jwkset/storage.go
generated
vendored
@@ -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()
|
||||
|
||||
12
vendor/github.com/cention-sany/utf7/.travis.yml
generated
vendored
Normal file
12
vendor/github.com/cention-sany/utf7/.travis.yml
generated
vendored
Normal file
@@ -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
|
||||
|
||||
29
vendor/github.com/cention-sany/utf7/LICENSE
generated
vendored
Normal file
29
vendor/github.com/cention-sany/utf7/LICENSE
generated
vendored
Normal file
@@ -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.
|
||||
2
vendor/github.com/cention-sany/utf7/README.md
generated
vendored
Normal file
2
vendor/github.com/cention-sany/utf7/README.md
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# utf7 [](https://travis-ci.org/cention-sany/utf7) [](https://godoc.org/github.com/cention-sany/utf7) [](https://exago.io/project/github.com/cention-sany/utf7) [](https://exago.io/project/github.com/cention-sany/utf7)
|
||||
RFC 2152 - UTF7 encoding and decoding.
|
||||
518
vendor/github.com/cention-sany/utf7/utf7.go
generated
vendored
Normal file
518
vendor/github.com/cention-sany/utf7/utf7.go
generated
vendored
Normal file
@@ -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
|
||||
*/
|
||||
202
vendor/github.com/dustinkirkland/golang-petname/LICENSE
generated
vendored
Normal file
202
vendor/github.com/dustinkirkland/golang-petname/LICENSE
generated
vendored
Normal file
@@ -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.
|
||||
134
vendor/github.com/dustinkirkland/golang-petname/README.md
generated
vendored
Normal file
134
vendor/github.com/dustinkirkland/golang-petname/README.md
generated
vendored
Normal file
@@ -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.
|
||||
51
vendor/github.com/dustinkirkland/golang-petname/golang-petname.1
generated
vendored
Normal file
51
vendor/github.com/dustinkirkland/golang-petname/golang-petname.1
generated
vendored
Normal file
@@ -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 <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 \fI/usr/share/common-licenses/Apache-2.0\fP on Debian/Ubuntu systems.
|
||||
82
vendor/github.com/dustinkirkland/golang-petname/petname.go
generated
vendored
Normal file
82
vendor/github.com/dustinkirkland/golang-petname/petname.go
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
petname: library for generating human-readable, random names
|
||||
for objects (e.g. hostnames, containers, blobs)
|
||||
|
||||
Copyright 2014 Dustin Kirkland <dustin.kirkland@gmail.com>
|
||||
|
||||
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)
|
||||
}
|
||||
19
vendor/github.com/emersion/go-imap/v2/.build.yml
generated
vendored
Normal file
19
vendor/github.com/emersion/go-imap/v2/.build.yml
generated
vendored
Normal file
@@ -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 .)
|
||||
23
vendor/github.com/emersion/go-imap/v2/LICENSE
generated
vendored
Normal file
23
vendor/github.com/emersion/go-imap/v2/LICENSE
generated
vendored
Normal file
@@ -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.
|
||||
29
vendor/github.com/emersion/go-imap/v2/README.md
generated
vendored
Normal file
29
vendor/github.com/emersion/go-imap/v2/README.md
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# go-imap
|
||||
|
||||
[](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
|
||||
104
vendor/github.com/emersion/go-imap/v2/acl.go
generated
vendored
Normal file
104
vendor/github.com/emersion/go-imap/v2/acl.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
18
vendor/github.com/emersion/go-imap/v2/append.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/v2/append.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
205
vendor/github.com/emersion/go-imap/v2/capability.go
generated
vendored
Normal file
205
vendor/github.com/emersion/go-imap/v2/capability.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
9
vendor/github.com/emersion/go-imap/v2/copy.go
generated
vendored
Normal file
9
vendor/github.com/emersion/go-imap/v2/copy.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
6
vendor/github.com/emersion/go-imap/v2/create.go
generated
vendored
Normal file
6
vendor/github.com/emersion/go-imap/v2/create.go
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package imap
|
||||
|
||||
// CreateOptions contains options for the CREATE command.
|
||||
type CreateOptions struct {
|
||||
SpecialUse []MailboxAttr // requires CREATE-SPECIAL-USE
|
||||
}
|
||||
284
vendor/github.com/emersion/go-imap/v2/fetch.go
generated
vendored
Normal file
284
vendor/github.com/emersion/go-imap/v2/fetch.go
generated
vendored
Normal file
@@ -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)
|
||||
15
vendor/github.com/emersion/go-imap/v2/id.go
generated
vendored
Normal file
15
vendor/github.com/emersion/go-imap/v2/id.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
105
vendor/github.com/emersion/go-imap/v2/imap.go
generated
vendored
Normal file
105
vendor/github.com/emersion/go-imap/v2/imap.go
generated
vendored
Normal file
@@ -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
|
||||
138
vendor/github.com/emersion/go-imap/v2/imapclient/acl.go
generated
vendored
Normal file
138
vendor/github.com/emersion/go-imap/v2/imapclient/acl.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
58
vendor/github.com/emersion/go-imap/v2/imapclient/append.go
generated
vendored
Normal file
58
vendor/github.com/emersion/go-imap/v2/imapclient/append.go
generated
vendored
Normal file
@@ -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()
|
||||
}
|
||||
100
vendor/github.com/emersion/go-imap/v2/imapclient/authenticate.go
generated
vendored
Normal file
100
vendor/github.com/emersion/go-imap/v2/imapclient/authenticate.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
55
vendor/github.com/emersion/go-imap/v2/imapclient/capability.go
generated
vendored
Normal file
55
vendor/github.com/emersion/go-imap/v2/imapclient/capability.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
1215
vendor/github.com/emersion/go-imap/v2/imapclient/client.go
generated
vendored
Normal file
1215
vendor/github.com/emersion/go-imap/v2/imapclient/client.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
37
vendor/github.com/emersion/go-imap/v2/imapclient/copy.go
generated
vendored
Normal file
37
vendor/github.com/emersion/go-imap/v2/imapclient/copy.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
21
vendor/github.com/emersion/go-imap/v2/imapclient/create.go
generated
vendored
Normal file
21
vendor/github.com/emersion/go-imap/v2/imapclient/create.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
69
vendor/github.com/emersion/go-imap/v2/imapclient/enable.go
generated
vendored
Normal file
69
vendor/github.com/emersion/go-imap/v2/imapclient/enable.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
84
vendor/github.com/emersion/go-imap/v2/imapclient/expunge.go
generated
vendored
Normal file
84
vendor/github.com/emersion/go-imap/v2/imapclient/expunge.go
generated
vendored
Normal file
@@ -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()
|
||||
}
|
||||
1326
vendor/github.com/emersion/go-imap/v2/imapclient/fetch.go
generated
vendored
Normal file
1326
vendor/github.com/emersion/go-imap/v2/imapclient/fetch.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
163
vendor/github.com/emersion/go-imap/v2/imapclient/id.go
generated
vendored
Normal file
163
vendor/github.com/emersion/go-imap/v2/imapclient/id.go
generated
vendored
Normal file
@@ -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()
|
||||
}
|
||||
157
vendor/github.com/emersion/go-imap/v2/imapclient/idle.go
generated
vendored
Normal file
157
vendor/github.com/emersion/go-imap/v2/imapclient/idle.go
generated
vendored
Normal file
@@ -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()
|
||||
}
|
||||
259
vendor/github.com/emersion/go-imap/v2/imapclient/list.go
generated
vendored
Normal file
259
vendor/github.com/emersion/go-imap/v2/imapclient/list.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
205
vendor/github.com/emersion/go-imap/v2/imapclient/metadata.go
generated
vendored
Normal file
205
vendor/github.com/emersion/go-imap/v2/imapclient/metadata.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
74
vendor/github.com/emersion/go-imap/v2/imapclient/move.go
generated
vendored
Normal file
74
vendor/github.com/emersion/go-imap/v2/imapclient/move.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
110
vendor/github.com/emersion/go-imap/v2/imapclient/namespace.go
generated
vendored
Normal file
110
vendor/github.com/emersion/go-imap/v2/imapclient/namespace.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
176
vendor/github.com/emersion/go-imap/v2/imapclient/quota.go
generated
vendored
Normal file
176
vendor/github.com/emersion/go-imap/v2/imapclient/quota.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
401
vendor/github.com/emersion/go-imap/v2/imapclient/search.go
generated
vendored
Normal file
401
vendor/github.com/emersion/go-imap/v2/imapclient/search.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
100
vendor/github.com/emersion/go-imap/v2/imapclient/select.go
generated
vendored
Normal file
100
vendor/github.com/emersion/go-imap/v2/imapclient/select.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
84
vendor/github.com/emersion/go-imap/v2/imapclient/sort.go
generated
vendored
Normal file
84
vendor/github.com/emersion/go-imap/v2/imapclient/sort.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
83
vendor/github.com/emersion/go-imap/v2/imapclient/starttls.go
generated
vendored
Normal file
83
vendor/github.com/emersion/go-imap/v2/imapclient/starttls.go
generated
vendored
Normal file
@@ -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)
|
||||
}
|
||||
161
vendor/github.com/emersion/go-imap/v2/imapclient/status.go
generated
vendored
Normal file
161
vendor/github.com/emersion/go-imap/v2/imapclient/status.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
44
vendor/github.com/emersion/go-imap/v2/imapclient/store.go
generated
vendored
Normal file
44
vendor/github.com/emersion/go-imap/v2/imapclient/store.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
85
vendor/github.com/emersion/go-imap/v2/imapclient/thread.go
generated
vendored
Normal file
85
vendor/github.com/emersion/go-imap/v2/imapclient/thread.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
13
vendor/github.com/emersion/go-imap/v2/internal/acl.go
generated
vendored
Normal file
13
vendor/github.com/emersion/go-imap/v2/internal/acl.go
generated
vendored
Normal file
@@ -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)
|
||||
}
|
||||
306
vendor/github.com/emersion/go-imap/v2/internal/imapnum/numset.go
generated
vendored
Normal file
306
vendor/github.com/emersion/go-imap/v2/internal/imapnum/numset.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
654
vendor/github.com/emersion/go-imap/v2/internal/imapwire/decoder.go
generated
vendored
Normal file
654
vendor/github.com/emersion/go-imap/v2/internal/imapwire/decoder.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
341
vendor/github.com/emersion/go-imap/v2/internal/imapwire/encoder.go
generated
vendored
Normal file
341
vendor/github.com/emersion/go-imap/v2/internal/imapwire/encoder.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
47
vendor/github.com/emersion/go-imap/v2/internal/imapwire/imapwire.go
generated
vendored
Normal file
47
vendor/github.com/emersion/go-imap/v2/internal/imapwire/imapwire.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
39
vendor/github.com/emersion/go-imap/v2/internal/imapwire/num.go
generated
vendored
Normal file
39
vendor/github.com/emersion/go-imap/v2/internal/imapwire/num.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
170
vendor/github.com/emersion/go-imap/v2/internal/internal.go
generated
vendored
Normal file
170
vendor/github.com/emersion/go-imap/v2/internal/internal.go
generated
vendored
Normal file
@@ -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)
|
||||
}
|
||||
23
vendor/github.com/emersion/go-imap/v2/internal/sasl.go
generated
vendored
Normal file
23
vendor/github.com/emersion/go-imap/v2/internal/sasl.go
generated
vendored
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
118
vendor/github.com/emersion/go-imap/v2/internal/utf7/decoder.go
generated
vendored
Normal file
118
vendor/github.com/emersion/go-imap/v2/internal/utf7/decoder.go
generated
vendored
Normal file
@@ -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]
|
||||
}
|
||||
88
vendor/github.com/emersion/go-imap/v2/internal/utf7/encoder.go
generated
vendored
Normal file
88
vendor/github.com/emersion/go-imap/v2/internal/utf7/encoder.go
generated
vendored
Normal file
@@ -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()
|
||||
}
|
||||
13
vendor/github.com/emersion/go-imap/v2/internal/utf7/utf7.go
generated
vendored
Normal file
13
vendor/github.com/emersion/go-imap/v2/internal/utf7/utf7.go
generated
vendored
Normal file
@@ -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+,")
|
||||
30
vendor/github.com/emersion/go-imap/v2/list.go
generated
vendored
Normal file
30
vendor/github.com/emersion/go-imap/v2/list.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
14
vendor/github.com/emersion/go-imap/v2/namespace.go
generated
vendored
Normal file
14
vendor/github.com/emersion/go-imap/v2/namespace.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
149
vendor/github.com/emersion/go-imap/v2/numset.go
generated
vendored
Normal file
149
vendor/github.com/emersion/go-imap/v2/numset.go
generated
vendored
Normal file
@@ -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))
|
||||
}
|
||||
13
vendor/github.com/emersion/go-imap/v2/quota.go
generated
vendored
Normal file
13
vendor/github.com/emersion/go-imap/v2/quota.go
generated
vendored
Normal file
@@ -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"
|
||||
)
|
||||
81
vendor/github.com/emersion/go-imap/v2/response.go
generated
vendored
Normal file
81
vendor/github.com/emersion/go-imap/v2/response.go
generated
vendored
Normal file
@@ -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 = "<unknown>"
|
||||
}
|
||||
fmt.Fprintf(&sb, " %v", text)
|
||||
return sb.String()
|
||||
}
|
||||
202
vendor/github.com/emersion/go-imap/v2/search.go
generated
vendored
Normal file
202
vendor/github.com/emersion/go-imap/v2/search.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
25
vendor/github.com/emersion/go-imap/v2/select.go
generated
vendored
Normal file
25
vendor/github.com/emersion/go-imap/v2/select.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
33
vendor/github.com/emersion/go-imap/v2/status.go
generated
vendored
Normal file
33
vendor/github.com/emersion/go-imap/v2/status.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
22
vendor/github.com/emersion/go-imap/v2/store.go
generated
vendored
Normal file
22
vendor/github.com/emersion/go-imap/v2/store.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
9
vendor/github.com/emersion/go-imap/v2/thread.go
generated
vendored
Normal file
9
vendor/github.com/emersion/go-imap/v2/thread.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
package imap
|
||||
|
||||
// ThreadAlgorithm is a threading algorithm.
|
||||
type ThreadAlgorithm string
|
||||
|
||||
const (
|
||||
ThreadOrderedSubject ThreadAlgorithm = "ORDEREDSUBJECT"
|
||||
ThreadReferences ThreadAlgorithm = "REFERENCES"
|
||||
)
|
||||
20
vendor/github.com/emersion/go-message/.build.yml
generated
vendored
Normal file
20
vendor/github.com/emersion/go-message/.build.yml
generated
vendored
Normal file
@@ -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 .)
|
||||
24
vendor/github.com/emersion/go-message/.gitignore
generated
vendored
Normal file
24
vendor/github.com/emersion/go-message/.gitignore
generated
vendored
Normal file
@@ -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
|
||||
21
vendor/github.com/emersion/go-message/LICENSE
generated
vendored
Normal file
21
vendor/github.com/emersion/go-message/LICENSE
generated
vendored
Normal file
@@ -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.
|
||||
31
vendor/github.com/emersion/go-message/README.md
generated
vendored
Normal file
31
vendor/github.com/emersion/go-message/README.md
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# go-message
|
||||
|
||||
[](https://pkg.go.dev/github.com/emersion/go-message)
|
||||
[](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
|
||||
66
vendor/github.com/emersion/go-message/charset.go
generated
vendored
Normal file
66
vendor/github.com/emersion/go-message/charset.go
generated
vendored
Normal file
@@ -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)
|
||||
}
|
||||
151
vendor/github.com/emersion/go-message/encoding.go
generated
vendored
Normal file
151
vendor/github.com/emersion/go-message/encoding.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
264
vendor/github.com/emersion/go-message/entity.go
generated
vendored
Normal file
264
vendor/github.com/emersion/go-message/entity.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
118
vendor/github.com/emersion/go-message/header.go
generated
vendored
Normal file
118
vendor/github.com/emersion/go-message/header.go
generated
vendored
Normal file
@@ -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)}
|
||||
}
|
||||
42
vendor/github.com/emersion/go-message/mail/address.go
generated
vendored
Normal file
42
vendor/github.com/emersion/go-message/mail/address.go
generated
vendored
Normal file
@@ -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 <bg@example.com>"
|
||||
// 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)
|
||||
}
|
||||
30
vendor/github.com/emersion/go-message/mail/attachment.go
generated
vendored
Normal file
30
vendor/github.com/emersion/go-message/mail/attachment.go
generated
vendored
Normal file
@@ -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)
|
||||
}
|
||||
381
vendor/github.com/emersion/go-message/mail/header.go
generated
vendored
Normal file
381
vendor/github.com/emersion/go-message/mail/header.go
generated
vendored
Normal file
@@ -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()}
|
||||
}
|
||||
10
vendor/github.com/emersion/go-message/mail/inline.go
generated
vendored
Normal file
10
vendor/github.com/emersion/go-message/mail/inline.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
// A InlineHeader represents a message text header.
|
||||
type InlineHeader struct {
|
||||
message.Header
|
||||
}
|
||||
9
vendor/github.com/emersion/go-message/mail/mail.go
generated
vendored
Normal file
9
vendor/github.com/emersion/go-message/mail/mail.go
generated
vendored
Normal file
@@ -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
|
||||
130
vendor/github.com/emersion/go-message/mail/reader.go
generated
vendored
Normal file
130
vendor/github.com/emersion/go-message/mail/reader.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
132
vendor/github.com/emersion/go-message/mail/writer.go
generated
vendored
Normal file
132
vendor/github.com/emersion/go-message/mail/writer.go
generated
vendored
Normal file
@@ -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()
|
||||
}
|
||||
15
vendor/github.com/emersion/go-message/message.go
generated
vendored
Normal file
15
vendor/github.com/emersion/go-message/message.go
generated
vendored
Normal file
@@ -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
|
||||
116
vendor/github.com/emersion/go-message/multipart.go
generated
vendored
Normal file
116
vendor/github.com/emersion/go-message/multipart.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
677
vendor/github.com/emersion/go-message/textproto/header.go
generated
vendored
Normal file
677
vendor/github.com/emersion/go-message/textproto/header.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
474
vendor/github.com/emersion/go-message/textproto/multipart.go
generated
vendored
Normal file
474
vendor/github.com/emersion/go-message/textproto/multipart.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
2
vendor/github.com/emersion/go-message/textproto/textproto.go
generated
vendored
Normal file
2
vendor/github.com/emersion/go-message/textproto/textproto.go
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package textproto implements low-level manipulation of MIME messages.
|
||||
package textproto
|
||||
134
vendor/github.com/emersion/go-message/writer.go
generated
vendored
Normal file
134
vendor/github.com/emersion/go-message/writer.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
19
vendor/github.com/emersion/go-sasl/.build.yml
generated
vendored
Normal file
19
vendor/github.com/emersion/go-sasl/.build.yml
generated
vendored
Normal file
@@ -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
|
||||
24
vendor/github.com/emersion/go-sasl/.gitignore
generated
vendored
Normal file
24
vendor/github.com/emersion/go-sasl/.gitignore
generated
vendored
Normal file
@@ -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
|
||||
21
vendor/github.com/emersion/go-sasl/LICENSE
generated
vendored
Normal file
21
vendor/github.com/emersion/go-sasl/LICENSE
generated
vendored
Normal file
@@ -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.
|
||||
18
vendor/github.com/emersion/go-sasl/README.md
generated
vendored
Normal file
18
vendor/github.com/emersion/go-sasl/README.md
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# go-sasl
|
||||
|
||||
[](https://godocs.io/github.com/emersion/go-sasl)
|
||||
[](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
|
||||
56
vendor/github.com/emersion/go-sasl/anonymous.go
generated
vendored
Normal file
56
vendor/github.com/emersion/go-sasl/anonymous.go
generated
vendored
Normal file
@@ -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}
|
||||
}
|
||||
67
vendor/github.com/emersion/go-sasl/external.go
generated
vendored
Normal file
67
vendor/github.com/emersion/go-sasl/external.go
generated
vendored
Normal file
@@ -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}
|
||||
}
|
||||
89
vendor/github.com/emersion/go-sasl/login.go
generated
vendored
Normal file
89
vendor/github.com/emersion/go-sasl/login.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
||||
198
vendor/github.com/emersion/go-sasl/oauthbearer.go
generated
vendored
Normal file
198
vendor/github.com/emersion/go-sasl/oauthbearer.go
generated
vendored
Normal file
@@ -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}
|
||||
}
|
||||
77
vendor/github.com/emersion/go-sasl/plain.go
generated
vendored
Normal file
77
vendor/github.com/emersion/go-sasl/plain.go
generated
vendored
Normal file
@@ -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}
|
||||
}
|
||||
45
vendor/github.com/emersion/go-sasl/sasl.go
generated
vendored
Normal file
45
vendor/github.com/emersion/go-sasl/sasl.go
generated
vendored
Normal file
@@ -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)
|
||||
}
|
||||
21
vendor/github.com/go-crypt/crypt/LICENSE
generated
vendored
Normal file
21
vendor/github.com/go-crypt/crypt/LICENSE
generated
vendored
Normal file
@@ -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.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user