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:
Pascal Bleser
2025-09-04 22:48:05 +02:00
parent 1b732b8bff
commit a8c2beac3a
204 changed files with 24588 additions and 3 deletions

14
go.mod
View File

@@ -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
View File

@@ -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=

View 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)
}
}
}
}

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,2 @@
# utf7 [![Build Status](https://travis-ci.org/cention-sany/utf7.png?branch=master)](https://travis-ci.org/cention-sany/utf7) [![GoDoc](https://godoc.org/github.com/cention-sany/utf7?status.png)](https://godoc.org/github.com/cention-sany/utf7) [![Exago](https://api.exago.io:443/badge/cov/github.com/cention-sany/utf7)](https://exago.io/project/github.com/cention-sany/utf7) [![Exago](https://api.exago.io:443/badge/rank/github.com/cention-sany/utf7)](https://exago.io/project/github.com/cention-sany/utf7)
RFC 2152 - UTF7 encoding and decoding.

518
vendor/github.com/cention-sany/utf7/utf7.go generated vendored Normal file
View 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+0020U+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
View 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.

View 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 Zookos 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 &lt;dustin.kirkland@gmail.com&gt; 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.

View 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.

View 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
View 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
View 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
View File

@@ -0,0 +1,29 @@
# go-imap
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-imap/v2.svg)](https://pkg.go.dev/github.com/emersion/go-imap/v2)
An [IMAP4rev2] library for Go.
> **Note**
> This is the README for go-imap v2. This new major version is still in
> development. For go-imap v1, see the [v1 branch].
## Usage
To add go-imap to your project, run:
go get github.com/emersion/go-imap/v2
Documentation and examples for the module are available here:
- [Client docs]
- [Server docs]
## License
MIT
[IMAP4rev2]: https://www.rfc-editor.org/rfc/rfc9051.html
[v1 branch]: https://github.com/emersion/go-imap/tree/v1
[Client docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapclient
[Server docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapserver

104
vendor/github.com/emersion/go-imap/v2/acl.go generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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()
}

View 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
}

View 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
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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
}

View 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
}

View 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

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
View 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()
}

View 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()
}

View 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
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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, &not)
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(&not) {
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
}

View 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
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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
View 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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
View 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)
}
}

View 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]
}

View 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()
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,31 @@
# go-message
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-message.svg)](https://pkg.go.dev/github.com/emersion/go-message)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-message/commits/master.svg)](https://builds.sr.ht/~emersion/go-message/commits/master?)
A Go library for the Internet Message Format. It implements:
* [RFC 5322]: Internet Message Format
* [RFC 2045], [RFC 2046] and [RFC 2047]: Multipurpose Internet Mail Extensions
* [RFC 2183]: Content-Disposition Header Field
## Features
* Streaming API
* Automatic encoding and charset handling (to decode all charsets, add
`import _ "github.com/emersion/go-message/charset"` to your application)
* A [`mail`](https://godocs.io/github.com/emersion/go-message/mail) subpackage
to read and write mail messages
* DKIM-friendly
* A [`textproto`](https://godocs.io/github.com/emersion/go-message/textproto)
subpackage that just implements the wire format
## License
MIT
[RFC 5322]: https://tools.ietf.org/html/rfc5322
[RFC 2045]: https://tools.ietf.org/html/rfc2045
[RFC 2046]: https://tools.ietf.org/html/rfc2046
[RFC 2047]: https://tools.ietf.org/html/rfc2047
[RFC 2183]: https://tools.ietf.org/html/rfc2183

66
vendor/github.com/emersion/go-message/charset.go generated vendored Normal file
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
}

View 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
}

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,18 @@
# go-sasl
[![godocs.io](https://godocs.io/github.com/emersion/go-sasl?status.svg)](https://godocs.io/github.com/emersion/go-sasl)
[![Build Status](https://travis-ci.org/emersion/go-sasl.svg?branch=master)](https://travis-ci.org/emersion/go-sasl)
A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go.
Implemented mechanisms:
* [ANONYMOUS](https://tools.ietf.org/html/rfc4505)
* [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A)
* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead)
* [PLAIN](https://tools.ietf.org/html/rfc4616)
* [OAUTHBEARER](https://tools.ietf.org/html/rfc7628)
## License
MIT

56
vendor/github.com/emersion/go-sasl/anonymous.go generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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