diff --git a/conf/conf.go b/conf/conf.go index f50b354..fd8f314 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -2,6 +2,7 @@ package conf import ( "github.com/1f349/lavender/issuer" + "github.com/1f349/lavender/mail" ) type Conf struct { @@ -10,5 +11,7 @@ type Conf struct { ServiceName string `yaml:"serviceName"` Issuer string `yaml:"issuer"` Kid string `yaml:"kid"` + Namespace string `yaml:"namespace"` + Mail mail.Mail `yaml:"mail"` SsoServices []issuer.SsoConfig `yaml:"ssoServices"` } diff --git a/go.mod b/go.mod index 00a2837..2681e02 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,12 @@ require ( github.com/1f349/cache v0.0.3 github.com/1f349/mjwt v0.4.1 github.com/1f349/overlapfs v0.0.1 + github.com/1f349/tulip v0.0.0-20240725211619-6b19e2d4ca63 github.com/charmbracelet/log v0.4.0 github.com/cloudflare/tableflip v1.2.3 + github.com/emersion/go-message v0.18.1 + github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 + github.com/emersion/go-smtp v0.21.3 github.com/go-oauth2/oauth2/v4 v4.5.2 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-migrate/migrate/v4 v4.17.1 @@ -17,7 +21,9 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 github.com/spf13/afero v1.11.0 github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.26.0 golang.org/x/oauth2 v0.22.0 + golang.org/x/text v0.17.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -31,7 +37,7 @@ require ( github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/klauspost/compress v1.17.9 // indirect @@ -53,10 +59,8 @@ require ( github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index 2fbd25d..a260d02 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/1f349/overlapfs v0.0.1 h1:LAxBolrXFAgU0yqZtXg/C/aaPq3eoQSPpBc49BHuTp0 github.com/1f349/overlapfs v0.0.1/go.mod h1:I6aItQycr7nrzplmfNXp/QF9tTmKRSgY3fXmu/7Ky2o= github.com/1f349/rsa-helper v0.0.2 h1:N/fLQqg5wrjIzG6G4zdwa5Xcv9/jIPutCls9YekZr9U= github.com/1f349/rsa-helper v0.0.2/go.mod h1:VUQ++1tYYhYrXeOmVFkQ82BegR24HQEJHl5lHbjg7yg= +github.com/1f349/tulip v0.0.0-20240725211619-6b19e2d4ca63 h1:jPg+0bgKD5kY7yQtRZqeba+BGKFE51evGvwewZwa7Xc= +github.com/1f349/tulip v0.0.0-20240725211619-6b19e2d4ca63/go.mod h1:1zFQhcbgiyPSWHVMp0cXJjmd6FhasP5bf5tWS4ZK61A= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= @@ -27,6 +29,13 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +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/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY= +github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -72,8 +81,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -193,29 +202,40 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -233,19 +253,30 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/lists/locales.go b/lists/locales.go new file mode 100644 index 0000000..abf8fdc --- /dev/null +++ b/lists/locales.go @@ -0,0 +1,104 @@ +package lists + +import ( + "golang.org/x/text/language" + "golang.org/x/text/language/display" + "sync" +) + +var ( + localeOnce sync.Once + localeNames []struct{ Value, Label string } +) + +func ListLocale() []struct{ Value, Label string } { + localeOnce.Do(func() { + localeNames = make([]struct{ Value, Label string }, len(localeList)) + for i := range localeList { + localeNames[i] = struct{ Value, Label string }{Value: localeList[i].String(), Label: display.Self.Name(localeList[i])} + } + }) + return localeNames +} + +var localeList = []language.Tag{ + language.Afrikaans, + language.Amharic, + language.Arabic, + language.ModernStandardArabic, + language.Azerbaijani, + language.Bulgarian, + language.Bengali, + language.Catalan, + language.Czech, + language.Danish, + language.German, + language.Greek, + language.English, + language.AmericanEnglish, + language.BritishEnglish, + language.Spanish, + language.EuropeanSpanish, + language.LatinAmericanSpanish, + language.Estonian, + language.Persian, + language.Finnish, + language.Filipino, + language.French, + language.CanadianFrench, + language.Gujarati, + language.Hebrew, + language.Hindi, + language.Croatian, + language.Hungarian, + language.Armenian, + language.Indonesian, + language.Icelandic, + language.Italian, + language.Japanese, + language.Georgian, + language.Kazakh, + language.Khmer, + language.Kannada, + language.Korean, + language.Kirghiz, + language.Lao, + language.Lithuanian, + language.Latvian, + language.Macedonian, + language.Malayalam, + language.Mongolian, + language.Marathi, + language.Malay, + language.Burmese, + language.Nepali, + language.Dutch, + language.Norwegian, + language.Punjabi, + language.Polish, + language.Portuguese, + language.BrazilianPortuguese, + language.EuropeanPortuguese, + language.Romanian, + language.Russian, + language.Sinhala, + language.Slovak, + language.Slovenian, + language.Albanian, + language.Serbian, + language.SerbianLatin, + language.Swedish, + language.Swahili, + language.Tamil, + language.Telugu, + language.Thai, + language.Turkish, + language.Ukrainian, + language.Urdu, + language.Uzbek, + language.Vietnamese, + language.Chinese, + language.SimplifiedChinese, + language.TraditionalChinese, + language.Zulu, +} diff --git a/lists/locales_test.go b/lists/locales_test.go new file mode 100644 index 0000000..d97a3d1 --- /dev/null +++ b/lists/locales_test.go @@ -0,0 +1,15 @@ +package lists + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestListLocale(t *testing.T) { + locales := ListLocale() + assert.True(t, len(locales) > 4) + assert.Equal(t, struct{ Value, Label string }{Value: "af", Label: "Afrikaans"}, locales[0]) + assert.Equal(t, struct{ Value, Label string }{Value: "am", Label: "አማርኛ"}, locales[1]) + assert.Equal(t, struct{ Value, Label string }{Value: "zh-Hant", Label: "繁體中文"}, locales[len(locales)-2]) + assert.Equal(t, struct{ Value, Label string }{Value: "zu", Label: "isiZulu"}, locales[len(locales)-1]) +} diff --git a/lists/zoneinfo.go b/lists/zoneinfo.go new file mode 100644 index 0000000..96d378b --- /dev/null +++ b/lists/zoneinfo.go @@ -0,0 +1,53 @@ +package lists + +import ( + "os" + "path/filepath" + "sort" + "strings" + "sync" +) + +var ( + zoneDirs = []string{ + // Update path according to your OS + "/usr/share/zoneinfo/", + "/usr/share/lib/zoneinfo/", + "/usr/lib/locale/TZ/", + } + zoneInfoOnce sync.Once + zoneNames []string +) + +func ListZoneInfo() []string { + zoneInfoOnce.Do(func() { + zoneNames = make([]string, 0) + for _, zoneDir := range zoneDirs { + zoneNames = append(zoneNames, FindTimeZoneFiles(zoneDir)...) + } + sort.Strings(zoneNames) + }) + return zoneNames +} + +func FindTimeZoneFiles(zoneDir string) []string { + dArr := make([]string, 0) + dArr = append(dArr, "") + arr := make([]string, 0) + + for i := 0; i < len(dArr); i++ { + dir := dArr[i] + files, _ := os.ReadDir(filepath.Join(zoneDir, dir)) + for _, f := range files { + if f.Name() != strings.ToUpper(f.Name()[:1])+f.Name()[1:] { + continue + } + if f.IsDir() { + dArr = append(dArr, filepath.Join(dir, f.Name())) + } else { + arr = append(arr, filepath.Join(dir, f.Name())) + } + } + } + return arr +} diff --git a/lists/zoneinfo_test.go b/lists/zoneinfo_test.go new file mode 100644 index 0000000..83e594b --- /dev/null +++ b/lists/zoneinfo_test.go @@ -0,0 +1,15 @@ +package lists + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestListZoneInfo(t *testing.T) { + zoneinfos := ListZoneInfo() + assert.True(t, len(zoneinfos) > 4) + assert.Equal(t, "Africa/Abidjan", zoneinfos[0]) + assert.Equal(t, "Africa/Accra", zoneinfos[1]) + assert.Equal(t, "WET", zoneinfos[len(zoneinfos)-2]) + assert.Equal(t, "Zulu", zoneinfos[len(zoneinfos)-1]) +} diff --git a/mail/from-address.go b/mail/from-address.go new file mode 100644 index 0000000..e52f5f8 --- /dev/null +++ b/mail/from-address.go @@ -0,0 +1,26 @@ +package mail + +import ( + "encoding/json" + "github.com/emersion/go-message/mail" +) + +type FromAddress struct { + *mail.Address +} + +var _ json.Unmarshaler = &FromAddress{} + +func (f *FromAddress) UnmarshalJSON(b []byte) error { + var a string + err := json.Unmarshal(b, &a) + if err != nil { + return err + } + address, err := mail.ParseAddress(a) + if err != nil { + return err + } + f.Address = address + return nil +} diff --git a/mail/mail.go b/mail/mail.go new file mode 100644 index 0000000..8403664 --- /dev/null +++ b/mail/mail.go @@ -0,0 +1,96 @@ +package mail + +import ( + "bytes" + "github.com/emersion/go-message/mail" + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" + "io" + "net" + "time" +) + +type Mail struct { + Name string `json:"name"` + Tls bool `json:"tls"` + Server string `json:"server"` + From FromAddress `json:"from"` + Username string `json:"username"` + Password string `json:"password"` +} + +func (m *Mail) loginInfo() sasl.Client { + return sasl.NewPlainClient("", m.Username, m.Password) +} + +func (m *Mail) mailCall(to []string, r io.Reader) error { + host, _, err := net.SplitHostPort(m.Server) + if err != nil { + return err + } + if m.Tls { + return smtp.SendMailTLS(m.Server, m.loginInfo(), m.From.String(), to, r) + } + if host == "localhost" || host == "127.0.0.1" { + // internals of smtp.SendMail without STARTTLS for localhost testing + dial, err := smtp.Dial(m.Server) + if err != nil { + return err + } + err = dial.Auth(m.loginInfo()) + if err != nil { + return err + } + return dial.SendMail(m.From.String(), to, r) + } + return smtp.SendMail(m.Server, m.loginInfo(), m.From.String(), to, r) +} + +func (m *Mail) SendMail(subject string, to []*mail.Address, htmlBody, textBody io.Reader) error { + // generate the email in this template + buf := new(bytes.Buffer) + + // setup mail headers + var h mail.Header + h.SetDate(time.Now()) + h.SetSubject(subject) + h.SetAddressList("From", []*mail.Address{m.From.Address}) + h.SetAddressList("To", to) + h.Set("Content-Type", "multipart/alternative") + + // setup html and text alternative headers + var hHtml, hTxt mail.InlineHeader + hHtml.Set("Content-Type", "text/html; charset=utf-8") + hTxt.Set("Content-Type", "text/plain; charset=utf-8") + + createWriter, err := mail.CreateWriter(buf, h) + if err != nil { + return err + } + inline, err := createWriter.CreateInline() + if err != nil { + return err + } + partHtml, err := inline.CreatePart(hHtml) + if err != nil { + return err + } + if _, err := io.Copy(partHtml, htmlBody); err != nil { + return err + } + partTxt, err := inline.CreatePart(hTxt) + if err != nil { + return err + } + if _, err := io.Copy(partTxt, textBody); err != nil { + return err + } + + // convert all to addresses to strings + toStr := make([]string, len(to)) + for i := range toStr { + toStr[i] = to[i].String() + } + + return m.mailCall(toStr, buf) +} diff --git a/mail/send-template.go b/mail/send-template.go new file mode 100644 index 0000000..5f2c22f --- /dev/null +++ b/mail/send-template.go @@ -0,0 +1,18 @@ +package mail + +import ( + "bytes" + "fmt" + "github.com/1f349/lavender/mail/templates" + "github.com/emersion/go-message/mail" +) + +func (m *Mail) SendEmailTemplate(templateName, subject, nameOfUser string, to *mail.Address, data map[string]any) error { + var bufHtml, bufTxt bytes.Buffer + templates.RenderMailTemplate(&bufHtml, &bufTxt, templateName, map[string]any{ + "ServiceName": m.Name, + "Name": nameOfUser, + "Data": data, + }) + return m.SendMail(fmt.Sprintf("%s - %s", subject, m.Name), []*mail.Address{to}, &bufHtml, &bufTxt) +} diff --git a/mail/templates/mail-account-delete.go.html b/mail/templates/mail-account-delete.go.html new file mode 100644 index 0000000..1b795d2 --- /dev/null +++ b/mail/templates/mail-account-delete.go.html @@ -0,0 +1,10 @@ + + + +

Hello, {{.Name}}

+

Your account with {{.ServiceName}} has been disabled and marked for deletion.

+

Your account will be fully deleted within 48-hours.

+

You will no longer receive emails from {{.ServiceName}}, unless your email address is used to set up an account.

+

Regards,
{{.ServiceName}}

+ + diff --git a/mail/templates/mail-account-delete.go.txt b/mail/templates/mail-account-delete.go.txt new file mode 100644 index 0000000..6fed9ed --- /dev/null +++ b/mail/templates/mail-account-delete.go.txt @@ -0,0 +1,10 @@ +Hello, {{.Name}} + +Your account with {{.ServiceName}} has been disabled and marked for deletion. + +Your account will be fully deleted within 48-hours. + +You will no longer receive emails from {{.ServiceName}}, unless your email address is used to set up an account. + +Regards, +{{.ServiceName}} diff --git a/mail/templates/mail-register-admin.go.html b/mail/templates/mail-register-admin.go.html new file mode 100644 index 0000000..62480d0 --- /dev/null +++ b/mail/templates/mail-register-admin.go.html @@ -0,0 +1,11 @@ + + + +

Hello, {{.Name}}

+

Your email address has been registered with {{.ServiceName}} by an administrator.

+

Please open this link to verify your email address and register your account

+

If you did not wish to register for {{.ServiceName}}, then your email has probably been used by mistake.

+

If the link above is not used to register an account, then no further contact will be made from {{.ServiceName}} and your email address will be deleted from our systems within a 48-hour period.

+

Regards,
{{.ServiceName}}

+ + diff --git a/mail/templates/mail-register-admin.go.txt b/mail/templates/mail-register-admin.go.txt new file mode 100644 index 0000000..d614035 --- /dev/null +++ b/mail/templates/mail-register-admin.go.txt @@ -0,0 +1,12 @@ +Hello, {{.Name}} + +Your email address has been registered with {{.ServiceName}} by an administrator. + +Please open this link to verify your email address and register your account {{.Data.RegisterUrl}} + +If you did not wish to register for {{.ServiceName}}, then your email has probably been used by mistake. + +If the link above is not used to register an account, then no further contact will be made from {{.ServiceName}} and your email address will be deleted from our systems within a 48-hour period. + +Regards, +{{.ServiceName}} diff --git a/mail/templates/mail-reset-password.go.html b/mail/templates/mail-reset-password.go.html new file mode 100644 index 0000000..b511045 --- /dev/null +++ b/mail/templates/mail-reset-password.go.html @@ -0,0 +1,9 @@ + + + +

Hello, {{.Name}}

+

Please open this link to reset your password

+

This link is valid for 10 minutes.

+

Regards,
{{.ServiceName}}

+ + diff --git a/mail/templates/mail-reset-password.go.txt b/mail/templates/mail-reset-password.go.txt new file mode 100644 index 0000000..da735dd --- /dev/null +++ b/mail/templates/mail-reset-password.go.txt @@ -0,0 +1,8 @@ +Hello, {{.Name}} + +Please open this link to reset your password: {{.Data.ResetUrl}} + +This link is valid for 10 minutes. + +Regards, +{{.ServiceName}} diff --git a/mail/templates/mail-verify.go.html b/mail/templates/mail-verify.go.html new file mode 100644 index 0000000..d4fc337 --- /dev/null +++ b/mail/templates/mail-verify.go.html @@ -0,0 +1,10 @@ + + + +

Hello, {{.Name}}

+

Please open this link to verify your email address

+

This link is valid for 10 minutes.

+

If you did not create an account with {{.ServiceName}} then please ignore this email and the account will be deleted within a 48-hour period.

+

Regards,
{{.ServiceName}}

+ + diff --git a/mail/templates/mail-verify.go.txt b/mail/templates/mail-verify.go.txt new file mode 100644 index 0000000..e8ddaa6 --- /dev/null +++ b/mail/templates/mail-verify.go.txt @@ -0,0 +1,10 @@ +Hello, {{.Name}} + +Please open this link to verify your email address: {{.Data.VerifyUrl}} + +This link is valid for 10 minutes. + +If you did not create an account with {{.ServiceName}} then please ignore this email and the account will be deleted within a 48-hour period. + +Regards, +{{.ServiceName}} diff --git a/mail/templates/templates.go b/mail/templates/templates.go new file mode 100644 index 0000000..ed82df3 --- /dev/null +++ b/mail/templates/templates.go @@ -0,0 +1,55 @@ +package templates + +import ( + "embed" + "errors" + "github.com/1f349/overlapfs" + "github.com/1f349/tulip/logger" + htmlTemplate "html/template" + "io" + "io/fs" + "os" + "path/filepath" + "sync" + textTemplate "text/template" +) + +var ( + //go:embed *.go.html *.go.txt + embeddedTemplates embed.FS + mailHtmlTemplates *htmlTemplate.Template + mailTextTemplates *textTemplate.Template + loadOnce sync.Once +) + +func LoadMailTemplates(wd string) (err error) { + loadOnce.Do(func() { + var o fs.FS = embeddedTemplates + if wd != "" { + mailDir := filepath.Join(wd, "mail-templates") + err = os.Mkdir(mailDir, os.ModePerm) + if err != nil && !errors.Is(err, os.ErrExist) { + return + } + wdFs := os.DirFS(mailDir) + o = overlapfs.OverlapFS{A: embeddedTemplates, B: wdFs} + } + mailHtmlTemplates, err = htmlTemplate.New("mail").ParseFS(o, "*.go.html") + if err != nil { + return + } + mailTextTemplates, err = textTemplate.New("mail").ParseFS(o, "*.go.txt") + }) + return +} + +func RenderMailTemplate(wrHtml, wrTxt io.Writer, name string, data any) { + err := mailHtmlTemplates.ExecuteTemplate(wrHtml, name+".go.html", data) + if err != nil { + logger.Logger.Warn("Failed to render mail html", "name", name, "err", err) + } + err = mailTextTemplates.ExecuteTemplate(wrTxt, name+".go.txt", data) + if err != nil { + logger.Logger.Warn("Failed to render mail text", "name", name, "err", err) + } +} diff --git a/password/password.go b/password/password.go new file mode 100644 index 0000000..d7f57e6 --- /dev/null +++ b/password/password.go @@ -0,0 +1,17 @@ +package password + +import ( + "golang.org/x/crypto/bcrypt" +) + +// HashString is used to represent a string containing a password hash +type HashString string + +func HashPassword(password string) (HashString, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return HashString(bytes), err +} + +func CheckPasswordHash(hash HashString, password string) error { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) +}