diff --git a/.env b/.env new file mode 100644 index 0000000..4a7c4a6 --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +{ + "SendmailFunction": { + "ReCAPTCHA_SECRET": "reCaptcha Secret Key", + + "MAIL_SUBJECT": "Test email", + "IS_HTML": "true", + "MAIL_FROM": "info@example.com", + "CC_ADDRESSES": ", Bob ", + "BCC_ADDRESSES": ", Bob ", + "TO_ADDRESSES": ", Bob ", + "SMTP_USER": "no-reply@e-business.co.jp", + "SMTP_PASSWORD": "password", + "SMTP_HOST": "sender.com", + "SMTP_PORT": "587" + } +} + diff --git a/.gitattributes b/.gitattributes index dfe0770..84227d4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ # Auto detect text files and perform LF normalization * text=auto +*.html linguist-detectable=false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9e1656a..face8b2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ main main.zip *.bak +.aws +.aws-sam +*.local +*/bak/* diff --git a/README.md b/README.md index 935e0af..776bd62 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,17 @@ Contact forms. They are all around us. Almost every website has one. Preparing a binary to deploy to AWS Lambda requires that it is compiled for Linux and placed into a .zip file. + +## Via docker + +``` shell +docker run -it --rm -v $(pwd):/work -w /work lambci/lambda:build-go1.x sam build +cd .aws-sam/build/SendmailFunction +zip main.zip main +``` + ## For developers on Linux and macOS + ``` shell # Remember to build your handler executable for Linux! GOOS=linux GOARCH=amd64 go build -o main main.go @@ -105,25 +115,25 @@ Let’s look at the [sample code](example.html) to understand it better. | Key | Value | | ------------- | ------------- | -| SENDMAIL_SUBJECT | email subject | -| SENDMAIL_MAILTYPE | html or plain | -| SENDER_USER | smtp user name | -| SENDER_NAME | the display sender name | -| SENDER_PASSWORD | smtp password | -| SENDER_HOST | smtp host(E.g., example.com) | -| SENDER_PORT | smtp port(E.g., 587) | -| RECIPIENTS | recipients (E.g., recipient ;recipient ;) | -| SECRET_KEY | reCaptcha Secret Key | - +| ReCAPTCHA_SECRET | reCaptcha Secret Key | +| SES_REGION | AWS Region (E.g.,'us-west-1') | +| MAIL_SUBJECT | email subject | +| IS_HTML | true or false | +| MAIL_FROM | from(E.g., recipient or info@site.com) | +| TO_ADDRESSES | recipients (E.g., recipient ,recipient ) | +| CC_ADDRESSES | recipients (E.g., recipient ,recipient ) | +| BCC_ADDRESSES | recipients (E.g., recipient ,recipient ) | +| SMTP_USER | smtp user name | +| SMTP_PASSWORD | smtp password | +| SMTP_HOST | smtp host(E.g., example.com) | +| SMTP_PORT | smtp port(E.g., 587) | ### Dev environment -- https://www.jianshu.com/p/166d272f51b3 -- ```sh sam init --runtime -sam local invoke SendmailFunction --event test-event.json +sam local invoke SendmailFunction --event test-event.json --env-vars .env # or sam local start-api diff --git a/build.sh b/build.sh index 99c964d..e11fe31 100644 --- a/build.sh +++ b/build.sh @@ -1,4 +1,8 @@ #!/bin/sh +# use sam +# docker run -it --rm -v $(pwd):/work -w /work -v $(pwd)/.aws:/root/.aws lambci/lambda:build-go1.x bash +# docker run -it --rm -v $(pwd):/work -w /work lambci/lambda:build-go1.x sam build + docker run -it --rm -v$(pwd):/work -w /work golang:alpine sh -c' apk add --update git zip GOOS=linux go build -v -ldflags '-d -s -w' -a -tags netgo -installsuffix netgo -o main main.go diff --git a/go.mod b/go.mod index 9d1b8ba..0df733a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,12 @@ module github.com/Xinguang/lambda-sendmail -go 1.12 +go 1.13 require ( - github.com/aws/aws-lambda-go v1.10.0 - github.com/sirupsen/logrus v1.4.1 + github.com/aws/aws-lambda-go v1.14.1 + github.com/aws/aws-sdk-go v1.29.29 + github.com/caarlos0/env v3.5.0+incompatible + github.com/sirupsen/logrus v1.4.2 + github.com/xinguang/go-recaptcha v1.0.1 + golang.org/x/tools v0.0.0-20200321224714-0d839f3cf2ed // indirect ) diff --git a/go.sum b/go.sum index f41a995..653ba95 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,80 @@ -github.com/aws/aws-lambda-go v1.10.0 h1:uafgdfYGQD0UeT7d2uKdyWW8j/ZYRifRPIdmeqLzLCk= -github.com/aws/aws-lambda-go v1.10.0/go.mod h1:zUsUQhAUjYzR8AuduJPCfhBuKWUaDbQiPOG+ouzmE1A= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aws/aws-lambda-go v1.14.1 h1:iklNeBEa1+EbcYVomAcZXXSEViwFzygvAPQ48TLvDlA= +github.com/aws/aws-lambda-go v1.14.1/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw= +github.com/aws/aws-sdk-go v1.29.29 h1:4TdSYzXL8bHKu80tzPjO4c0ALw4Fd8qZGqf1aozUcBU= +github.com/aws/aws-sdk-go v1.29.29/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= +github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= +github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/xinguang/go-recaptcha v1.0.1 h1:oB6dDxDYofvKl7Emdf/Wj5R9a7ffoMLpwlKW/u9+dRI= +github.com/xinguang/go-recaptcha v1.0.1/go.mod h1:SyVUtlgYY04YsLNZo7frgunPDJ1ZAkdddC0Joro7Xw0= +github.com/yuin/goldmark v1.1.25 h1:isv+Q6HQAmmL2Ofcmg8QauBmDPlUUnSoNhEcC940Rds= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200321224714-0d839f3cf2ed h1:OCZDlBlLYiUK6T33/8+3BnojrS2W+Dg1rKYJhR89xGE= +golang.org/x/tools v0.0.0-20200321224714-0d839f3cf2ed/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/recaptcha/lambdahandle.go b/handle.go similarity index 57% rename from recaptcha/lambdahandle.go rename to handle.go index adc48f3..1b224f5 100644 --- a/recaptcha/lambdahandle.go +++ b/handle.go @@ -1,4 +1,4 @@ -package recaptcha +package main import ( "net/mail" @@ -6,6 +6,7 @@ import ( "github.com/Xinguang/lambda-sendmail/sendmail" "github.com/aws/aws-lambda-go/events" log "github.com/sirupsen/logrus" + "github.com/xinguang/go-recaptcha" ) type Contact struct { @@ -15,27 +16,32 @@ type Contact struct { Token string `json:"token"` } -// Handle for verify a user's response to a reCAPTCHA challenge -func Handle(contact Contact) (events.APIGatewayProxyResponse, error) { - if !verify(contact.Token) { +var ( + captcha *recaptcha.ReCAPTCHA +) +func init() { + _captcha, err := recaptcha.New() + if err != nil { + log.Fatal("ReCAPTCHA:", err) + } + captcha = _captcha +} + +// handle for verify a user's response to a reCAPTCHA challenge +func handle(contact Contact) (events.APIGatewayProxyResponse, error) { + log.Debugf("Contact: %s", contact) + err := captcha.Verify(contact.Token) + if err != nil { return events.APIGatewayProxyResponse{ - Body: "timeout-or-duplicate", + Body: err.Error(), StatusCode: 400, }, nil } - log.Info(contact) reply := mail.Address{Name: contact.Name, Address: contact.Email} - message := sendmail.NewMessage() - - message.Add("Reply-To", reply.String()) - message.SetBody(contact.Message) - - log.Info("send email") - err := sendmail.Send(*message) + _, err = sendmail.Send(reply, contact.Message) if err != nil { - log.Fatal(err) return events.APIGatewayProxyResponse{ Body: "there is some errors", StatusCode: 400, diff --git a/main.go b/main.go index fa8d51f..03d917d 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "github.com/Xinguang/lambda-sendmail/recaptcha" "github.com/aws/aws-lambda-go/lambda" ) @@ -14,5 +13,5 @@ import ( // env GOOS=linux go build -v -ldflags '-d -s -w' -a -tags netgo -installsuffix netgo -o main main.go as a build command. func main() { // Make the handler available for Remote Procedure Call by AWS Lambda - lambda.Start(recaptcha.Handle) + lambda.Start(handle) } diff --git a/recaptcha/recaptcha.go b/recaptcha/recaptcha.go deleted file mode 100644 index b74742c..0000000 --- a/recaptcha/recaptcha.go +++ /dev/null @@ -1,60 +0,0 @@ -package recaptcha - -import ( - "bytes" - "encoding/json" - "io" - "io/ioutil" - "net/http" - "os" - "strings" - "time" - - log "github.com/sirupsen/logrus" -) - -var ( - SECRET = os.Getenv("SECRET_KEY") - URL_GOOGLE = "https://www.google.com/recaptcha/api/siteverify" -) - -type Response struct { - Success bool - ChallengeTS time.Time `json:"challenge_ts"` - Hostname string - ErrorCodes interface{} `json:"error-codes"` -} - -func verify(token string) bool { - var r http.Request - r.ParseForm() - r.Form.Add("secret", SECRET) - r.Form.Add("response", token) - body := strings.NewReader(r.Form.Encode()) - resp, err := http.Post(URL_GOOGLE, "application/x-www-form-urlencoded", body) - if err != nil { - log.Errorf("verify: %s", err) - return false - } - var res Response - err = unmarshal(resp.Body, &res) - log.Println(res) - return res.Success -} - -func unmarshal(body io.Reader, v interface{}) error { - bodyBytes, err := ioutil.ReadAll(body) - if err != nil { - log.Errorf("ioutil.ReadAll: %s", err) - return err - } - bodyBytes = bytes.TrimPrefix(bodyBytes, []byte("\xef\xbb\xbf")) - err = json.Unmarshal(bodyBytes, &v) - log.Println(v) - - if err != nil { - log.Errorf("unmarshal: %s", err) - return err - } - return nil -} diff --git a/sendmail/env.go b/sendmail/env.go new file mode 100644 index 0000000..a99d2c5 --- /dev/null +++ b/sendmail/env.go @@ -0,0 +1,30 @@ +package sendmail + +import ( + "github.com/caarlos0/env" + log "github.com/sirupsen/logrus" +) + +type ENV struct { + MailSubject string `env:"MAIL_SUBJECT" envDefault:"Test email"` + SesRegion string `env:"SES_REGION" envDefault:"us-west-1"` + IsHtml bool `env:"IS_HTML" envDefault:true` + MailFrom string `env:"MAIL_FROM"` + CcAddresses string `env:"CC_ADDRESSES"` + BccAddresses string `env:"BCC_ADDRESSES"` + ToAddresses string `env:"TO_ADDRESSES"` + SmtpUser string `env:"SMTP_USER"` + SmtpPassword string `env:"SMTP_PASSWORD"` + SmtpHost string `env:"SMTP_HOST"` + SmtpPort int `env:"SMTP_PORT" envDefault:587` +} +var ( + envHolder = ENV{} +) + +func init() { + err := env.Parse(&envHolder) + if err != nil { + log.Fatal(err) + } +} \ No newline at end of file diff --git a/sendmail/message.go b/sendmail/message.go deleted file mode 100644 index 7965f22..0000000 --- a/sendmail/message.go +++ /dev/null @@ -1,54 +0,0 @@ -package sendmail - -import ( - "encoding/base64" - "fmt" - "os" -) - -var ( - SENDMAIL_SUBJECT = os.Getenv("SENDMAIL_SUBJECT") - SENDMAIL_MAILTYPE = os.Getenv("SENDMAIL_MAILTYPE") // html , plain .... -) - -type Message struct { - header map[string]string - body string -} - -func NewMessage() *Message { - message := &Message{} - message.header = make(map[string]string) - message.Add("Subject", SENDMAIL_SUBJECT) - message.Add("MIME-Version", "1.0") - if len(SENDMAIL_MAILTYPE) > 0 { - message.Add("Content-Type", "text/"+SENDMAIL_MAILTYPE+"; charset=\"utf-8\"") - } else { - message.Add("Content-Type", "text/plain; charset=\"utf-8\"") - } - message.Add("Content-Transfer-Encoding", "base64") - return message -} - -func (msg *Message) Add(field, value string) { - msg.header[field] = value -} -func (msg *Message) SetBody(body string) { - msg.body = body -} - -// The msg parameter should be an RFC 822-style email with headers -// first, a blank line, and then the message body. The lines of msg -// should be CRLF terminated. The msg headers should usually include -// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc" -// messages is accomplished by including an email address in the to -// parameter but not including it in the msg headers. - -func (msg *Message) getContent() []byte { - content := "" - for k, v := range msg.header { - content += fmt.Sprintf("%s: %s\r\n", k, v) - } - content += "\r\n" + base64.StdEncoding.EncodeToString([]byte(msg.body)) - return []byte(content) -} diff --git a/sendmail/sendmail.go b/sendmail/sendmail.go index d3012dc..e1f3f99 100644 --- a/sendmail/sendmail.go +++ b/sendmail/sendmail.go @@ -1,120 +1,71 @@ package sendmail import ( - "crypto/tls" - "fmt" - "io" - "net/mail" - "net/smtp" - "os" - "strings" - log "github.com/sirupsen/logrus" + "net/mail" ) -var ( - SENDER_NAME = os.Getenv("SENDER_NAME") - SENDER_USER = os.Getenv("SENDER_USER") - SENDER_PASSWORD = os.Getenv("SENDER_PASSWORD") - SENDER_HOST = os.Getenv("SENDER_HOST") - SENDER_PORT = os.Getenv("SENDER_PORT") // default 587 - RECIPIENTS = os.Getenv("RECIPIENTS") // ;recipient ; -) - -// The recipient address -var recipients = func() []*mail.Address { - addressList := []*mail.Address{} - var mailList = strings.Split(RECIPIENTS, ";") - for _, mailString := range mailList { - recipient, err := mail.ParseAddress(mailString) - if err != nil { - continue - } - addressList = append(addressList, recipient) - } - return addressList -}() - -func Send(msg Message) error { - // The servername must include a port, as in "mail.example.com:smtp". - servername := fmt.Sprintf("%s:%s", SENDER_HOST, SENDER_PORT) - auth := smtp.PlainAuth("", SENDER_USER, SENDER_PASSWORD, SENDER_HOST) - toList := []string{} - for _, recipient := range recipients { - toList = append(toList, recipient.Address) - } - msg.Add("To", strings.Join(strings.Split(RECIPIENTS, ";"), ",")) - from := mail.Address{Name: SENDER_NAME, Address: SENDER_USER} - msg.Add("From", from.String()) - - log.Infof("recipients %s", recipients) - - return smtp.SendMail(servername, auth, from.Address, toList, msg.getContent()) +type SendMail struct { + env ENV + from *mail.Address + ccList []*mail.Address + bccList []*mail.Address + toList []*mail.Address } -// SendMail connects to the server at addr, switches to TLS if -// possible, authenticates with the optional mechanism a if possible, -// and then sends an email from address from, to addresses to, with -// message msg. -// -// The addresses in the to parameter are the SMTP RCPT addresses. -func SendMail(from string, msg Message) error { - client := dial() - defer client.Close() +var ( + sendmail = &SendMail{} +) - w := writeCloser(client, from) - _, err := w.Write(msg.getContent()) +func init() { + sendmail.env = envHolder + var err error + sendmail.from, err = mail.ParseAddress(envHolder.MailFrom) if err != nil { - return err + log.Fatal("MAIL_FROM:", err) } - err = w.Close() + sendmail.toList, err = parseAddressList(envHolder.ToAddresses) if err != nil { - return err + log.Fatalf("TO_ADDRESSES: %s error: %s", envHolder.ToAddresses, err) } - return client.Quit() -} - -func dial() *smtp.Client { - // The servername must include a port, as in "mail.example.com:smtp". - servername := fmt.Sprintf("%s:%s", SENDER_HOST, SENDER_PORT) - // TLS config - tlsconfig := &tls.Config{ - InsecureSkipVerify: true, - ServerName: SENDER_HOST, - } - // Here is the key, you need to call tls.Dial instead of smtp.Dial - // for smtp servers running on 465 that require an ssl connection - // from the very beginning (no starttls) - conn, err := tls.Dial("tcp", servername, tlsconfig) + sendmail.ccList, err = parseAddressList(envHolder.CcAddresses) if err != nil { - log.Fatal(err) + log.Fatal("CC_ADDRESSES:", err) } - client, err := smtp.NewClient(conn, SENDER_HOST) + sendmail.bccList, err = parseAddressList(envHolder.BccAddresses) if err != nil { - log.Fatal(err) + log.Fatal("BCC_ADDRESSES:", err) } - return client } -var writeCloser = func(client *smtp.Client, from string) io.WriteCloser { - auth := smtp.PlainAuth("", SENDER_USER, SENDER_PASSWORD, SENDER_HOST) - // Auth - if err := client.Auth(auth); err != nil { - log.Fatal(err) +func parseAddressList(list string) ([]*mail.Address, error) { + if len(list) == 0 { + return nil, nil } - // To && From - if err := client.Mail(from); err != nil { - log.Fatal(err) - } - for _, recipient := range recipients { - if err := client.Rcpt(recipient.Address); err != nil { - log.Fatal(err) - } - } - // Data - w, err := client.Data() - if err != nil { - log.Fatal(err) + return mail.ParseAddressList(list) +} + +func Send(reply mail.Address, message string) (*string, error) { + log.Info("sendmail", sendmail) + + log.Info("reply", reply) + if len(sendmail.env.SmtpHost) == 0 { + sender := newSESSender(sendmail) + return sender.Send(reply, message) + } else { + sender := newSMTPSender(sendmail) + err := sender.Send(reply, message) + return nil, err } - return w + return nil, nil + + // if len(SMTP_HOST)>0 { + // msg := sendmail.NewMessage() + + // msg.Add("Reply-To", reply.String()) + // msg.SetBody(message) + + // log.Info("send email") + // err = sendmail.Send(*message) + // } } diff --git a/sendmail/ses.go b/sendmail/ses.go new file mode 100644 index 0000000..c2f3cdd --- /dev/null +++ b/sendmail/ses.go @@ -0,0 +1,87 @@ +package sendmail + +import ( + log "github.com/sirupsen/logrus" + "net/mail" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ses" +) + +type sesSender struct { + client *ses.SES + destination *ses.Destination + source *string + isHtml bool + subject string +} + +func newSESSender(sendmail *SendMail) *sesSender { + // SES + config := aws.NewConfig().WithRegion(sendmail.env.SesRegion) + + sender := &sesSender{} + sender.client = ses.New(session.New(), config) + + sender.isHtml = sendmail.env.IsHtml + sender.subject = sendmail.env.MailSubject + sender.source = aws.String(sendmail.from.String()) + // ses.Destination + sender.destination = &ses.Destination{ + // The recipients to place on the To: line of the message. + ToAddresses: sender.parseAddressList(sendmail.toList), + // The recipients to place on the CC: line of the message. + CcAddresses: sender.parseAddressList(sendmail.ccList), + // The recipients to place on the BCC: line of the message. + BccAddresses: sender.parseAddressList(sendmail.bccList), + } + return sender +} +func (sender *sesSender) parseAddressList(emails []*mail.Address) []*string { + if emails == nil || len(emails) == 0 { + return nil + } + addressList := []*string{} + for _, v := range emails { + addressList = append(addressList, aws.String(v.String())) + } + return addressList +} + +func (sender *sesSender) getSendEmailInput(reply mail.Address, message string) *ses.SendEmailInput { + content := &ses.Content{ + Charset: aws.String("UTF-8"), + Data: aws.String(message), + } + body := &ses.Body{} + if sender.isHtml { + body.Html = content + } else { + body.Text = content + } + return &ses.SendEmailInput{ + Destination: sender.destination, + Message: &ses.Message{ + Body: body, + Subject: &ses.Content{ + Charset: aws.String("UTF-8"), + Data: aws.String(sender.subject), + }, + }, + ReplyToAddresses: []*string{ + aws.String(reply.String()), + }, + Source: sender.source, + } +} +func (sender *sesSender) Send(reply mail.Address, message string) (*string, error) { + log.Info("reply", reply) + log.Info("sender", sender) + input := sender.getSendEmailInput(reply, message) + result, err := sender.client.SendEmail(input) + if err != nil { + return nil, err + } + return result.MessageId, nil +} diff --git a/sendmail/smtp.go b/sendmail/smtp.go new file mode 100644 index 0000000..ad836c8 --- /dev/null +++ b/sendmail/smtp.go @@ -0,0 +1,108 @@ +package sendmail + +import ( + log "github.com/sirupsen/logrus" + "fmt" + "net/mail" + "net/smtp" + "strings" + "encoding/base64" +) + +type smtpSender struct { + header string + boundary string + body string + isHtml bool + + mailAuth smtp.Auth + // The servername must include a port, as in "mail.example.com:smtp". + servername string + from string + toList []string +} + + +func (sender *smtpSender) addHeader(field, value string) { + if field == "boundary" { + sender.header += fmt.Sprintf("%s\r\n", value) + } else { + sender.header += fmt.Sprintf("%s: %s\r\n", field, value) + } +} + +func (sender *smtpSender) setBody(body string) { + contentType := "plain" + if sender.isHtml { + contentType = "html" + } + sender.body = fmt.Sprintf("Content-Type: text/%s; charset=\"utf-8\"\r\n", contentType) + sender.body += "Content-Transfer-Encoding: base64\r\n" + sender.body += base64.StdEncoding.EncodeToString([]byte(body)) +} +// The msg parameter should be an RFC 822-style email with headers +// first, a blank line, and then the message body. The lines of msg +// should be CRLF terminated. The msg headers should usually include +// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc" +// messages is accomplished by including an email address in the to +// parameter but not including it in the msg headers. + +func (sender *smtpSender) getContent() []byte { + content := sender.header + // // mixed + // content += fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n", sender.boundary) + // // split line + // content += fmt.Sprintf("\r\n--%s\r\n", sender.boundary) + // mail body + content += sender.body + + return []byte(content) +} + +// Send connects to the server at addr, switches to TLS if +// possible, authenticates with the optional mechanism a if possible, +// and then sends an email from address from, to addresses to, with +// message msg. +// +// The addresses in the to parameter are the SMTP RCPT addresses. +func (sender *smtpSender) Send(reply mail.Address, message string) error { + log.Infof("Send with tls %s", reply) + err := sender.sendWithTls(reply, message) + if(err == nil) { + return nil + } + log.Infof("Send %s", reply) + sender.addHeader("Reply-To", reply.String()) + sender.setBody(message) + return smtp.SendMail(sender.servername, sender.mailAuth, sender.from, sender.toList, sender.getContent()) +} + +func (sender *smtpSender) parseAddressList(list []*mail.Address) string { + strList := []string{} + for _, m := range list { + strList = append(strList, m.Address) + } + return strings.Join(strList, ",") +} + +func newSMTPSender(sendmail *SendMail) *smtpSender { + sender := &smtpSender{} + sender.isHtml = sendmail.env.IsHtml + sender.addHeader("Subject", sendmail.env.MailSubject) + sender.addHeader("From", sendmail.from.String()) + sender.addHeader("To", sender.parseAddressList(sendmail.toList)) + sender.addHeader("Cc", sender.parseAddressList(sendmail.ccList)) + sender.addHeader("Bcc", sender.parseAddressList(sendmail.bccList)) + sender.addHeader("MIME-Version", "1.0") + + sender.mailAuth = smtp.PlainAuth("", sendmail.env.SmtpUser, sendmail.env.SmtpPassword, sendmail.env.SmtpHost) + // The servername must include a port, as in "mail.example.com:smtp". + sender.servername = fmt.Sprintf("%s:%d", sendmail.env.SmtpHost, sendmail.env.SmtpPort) + + sender.from = sendmail.from.Address + sender.toList = []string{} + for _, to := range sendmail.toList { + sender.toList = append(sender.toList, to.Address) + } + return sender +} diff --git a/sendmail/smtps.go b/sendmail/smtps.go new file mode 100644 index 0000000..87b033e --- /dev/null +++ b/sendmail/smtps.go @@ -0,0 +1,78 @@ +package sendmail + +import ( + log "github.com/sirupsen/logrus" + "net/mail" + "net/smtp" + "net" + "crypto/tls" + "io" +) + +func (sender *smtpSender) dial() (*smtp.Client, error) { + host, _, _ := net.SplitHostPort(sender.servername) + // TLS config + tlsconfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: host, + } + // Here is the key, you need to call tls.Dial instead of smtp.Dial + // for smtp servers running on 465 that require an ssl connection + // from the very beginning (no starttls) + conn, err := tls.Dial("tcp", sender.servername, tlsconfig) + if err != nil { + return nil, err + } + client, err := smtp.NewClient(conn, host) + if err != nil { + return nil, err + } + return client, nil +} + +// SendMail connects to the server at addr, switches to TLS if +// possible, authenticates with the optional mechanism a if possible, +// and then sends an email from address from, to addresses to, with +// message msg. +// +// The addresses in the to parameter are the SMTP RCPT addresses. +func (sender *smtpSender) sendWithTls(reply mail.Address, message string) error { + client, err := sender.dial() + if err != nil { + return err + } + defer client.Close() + + w := sender.writeCloser(client, sender.from) + _, err = w.Write(sender.getContent()) + if err != nil { + return err + } + err = w.Close() + if err != nil { + return err + } + return client.Quit() +} + +func (sender *smtpSender) writeCloser(client *smtp.Client, from string) io.WriteCloser { + // Auth + if err := client.Auth(sender.mailAuth); err != nil { + log.Fatal(err) + } + // To && From + if err := client.Mail(from); err != nil { + log.Fatal(err) + } + for _, to := range sender.toList { + if err := client.Rcpt(to); err != nil { + log.Fatal(err) + } + } + // Data + w, err := client.Data() + if err != nil { + log.Fatal(err) + } + return w +} \ No newline at end of file diff --git a/template.yaml b/template.yaml index 4adaea4..d77745b 100644 --- a/template.yaml +++ b/template.yaml @@ -26,25 +26,16 @@ Resources: Method: POST Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object Variables: - RECIPIENTS: ;recipient ; - SENDMAIL_SUBJECT: testMail - SENDMAIL_MAILTYPE: html - SENDER_NAME: sender - SENDER_USER: no-reply@sender.com - SENDER_PASSWORD: password - SENDER_HOST: sender.com - SENDER_PORT: 587 - -Outputs: - # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function - # Find out more about other implicit resources you can reference within SAM - # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api - HelloWorldAPI: - Description: "API Gateway endpoint URL for Prod environment for First Function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - HelloWorldFunction: - Description: "First Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn + ReCAPTCHA_SECRET: 'reCaptcha Secret Key' + SES_REGION: 'us-west-1' + MAIL_SUBJECT: 'Test email' + IS_HTML: true + MAIL_FROM: 'info@example.com' + CC_ADDRESSES: ', Bob ' + BCC_ADDRESSES: ', Bob ' + TO_ADDRESSES: ', Bob ' + SMTP_USER: 'no-reply@e-business.co.jp' + SMTP_PASSWORD: 'password' + SMTP_HOST: 'sender.com' + SMTP_PORT: '587' + \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..6930a27 --- /dev/null +++ b/test.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# test +# docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) -v $(pwd)/.aws:/root/.aws -v /var/run/docker.sock:/var/run/docker.sock lambci/lambda:build-go1.x bash +# build +docker run -it --rm -v $(pwd):/work -w /work lambci/lambda:build-go1.x sam build +# invoke +sam local invoke SendmailFunction --event test-event.json --env-vars .env.test \ No newline at end of file