2020-05-10 01:20:35 +02:00
/ *
Copyright 2020 Rodrigue Chakode and contributors .
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
2020-06-02 15:27:08 +02:00
2020-05-10 01:20:35 +02:00
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 .
* /
2020-05-10 00:02:18 +02:00
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"html/template"
"net"
"net/http"
"net/smtp"
2020-06-02 11:36:33 +02:00
"net/url"
2020-05-10 00:02:18 +02:00
"strings"
2021-09-19 22:49:48 +02:00
"log"
2020-05-10 00:02:18 +02:00
2020-06-02 15:27:08 +02:00
"github.com/dpapathanasiou/go-recaptcha"
2020-05-10 00:02:18 +02:00
"github.com/spf13/viper"
)
type SendMailRequest struct {
from string
to [ ] string
subject string
body string
}
2020-05-10 01:20:35 +02:00
type ContactRequest struct {
Name string ` json:"name,omitempty" `
Email string ` json:"email,omitempty" `
Organization string ` json:"organization,omitempty" `
Subject string ` json:"subject,omitempty" `
Message string ` json:"message,omitempty" `
RequestTarget string ` json:"requestType,omitempty" `
2021-10-26 20:14:24 +02:00
OriginURI string ` json:"originURI,omitempty" `
2020-05-10 01:20:35 +02:00
}
type ContactResponse struct {
Status string ` json:"status,omitempty" `
Message string ` json:"message,omitempty" `
}
2020-05-10 00:02:18 +02:00
// NewSendMailRequest creates a new instance to manage send mail
func NewSendMailRequest ( from string , to [ ] string , subject string ) * SendMailRequest {
return & SendMailRequest {
from : from ,
to : to ,
subject : subject ,
}
}
// Execute processes the actual email sending
func ( m * SendMailRequest ) Execute ( ) error {
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
from := "From: " + m . from + "\n"
subject := "Subject: " + m . subject + "\n"
msg := [ ] byte ( from + subject + mime + "\n" + m . body )
// Connect to the SMTP Server
smtpServerAddr := viper . GetString ( "SMTP_SERVER_ADDR" )
smtpServerHost , _ , _ := net . SplitHostPort ( smtpServerAddr )
smtpClientAuth := smtp . PlainAuth ( "" ,
viper . GetString ( "SMTP_CLIENT_USERNAME" ) ,
viper . GetString ( "SMTP_CLIENT_PASSWORD" ) ,
smtpServerHost )
// TLS config
tlsconfig := & tls . Config {
2020-05-14 14:58:22 +02:00
InsecureSkipVerify : viper . GetBool ( "SMTP_VERITY_CERT" ) ,
2020-05-10 00:02:18 +02:00
ServerName : smtpServerHost ,
}
// Important: call tls.Dial instead of smtp.Dial for smtp servers running on 465.
// On port 465 ssl connection is required from the very beginning (no starttls)
conn , err := tls . Dial ( "tcp" , smtpServerAddr , tlsconfig )
if err != nil {
2021-09-19 22:49:48 +02:00
return fmt . Errorf ( "failed initiating smtp connection to host %s (%s)" , smtpServerAddr , err )
2020-05-10 00:02:18 +02:00
}
2020-06-16 18:45:34 +02:00
defer conn . Close ( )
2020-05-10 00:02:18 +02:00
smtpClient , err := smtp . NewClient ( conn , smtpServerHost )
if err != nil {
2021-09-19 22:49:48 +02:00
return fmt . Errorf ( "failed creating smtp client to host %s (%s)" , smtpServerHost , err )
2020-05-10 00:02:18 +02:00
}
defer smtpClient . Quit ( )
if err = smtpClient . Auth ( smtpClientAuth ) ; err != nil {
return fmt . Errorf ( "failed authenticating to smtp server (%s)" , err )
}
// Initialize a mail transaction
err = smtpClient . Mail ( m . from )
if err != nil {
return fmt . Errorf ( "failed issuing MAIL command (%s)" , err )
}
2021-10-26 20:14:24 +02:00
// Set recipients
2020-05-10 00:02:18 +02:00
for _ , recipient := range m . to {
err = smtpClient . Rcpt ( recipient )
if err != nil {
return fmt . Errorf ( "failed issuing RCPT command (%s)" , err )
}
}
smtpWriter , err := smtpClient . Data ( )
if err != nil {
return fmt . Errorf ( "failed issuing DATA command (%s)" , err )
}
2020-06-16 15:20:59 +02:00
defer smtpWriter . Close ( )
2020-05-10 00:02:18 +02:00
_ , err = smtpWriter . Write ( [ ] byte ( msg ) )
if err != nil {
return fmt . Errorf ( "failed sending mail content (%s)" , err )
}
return nil
}
// ParseTemplate parses template and bing data and process the email sending
func ( m * SendMailRequest ) ParseTemplate ( templateFileName string , data interface { } ) error {
emailTpl , err := template . ParseFiles ( templateFileName )
if err != nil {
return err
}
buf := new ( bytes . Buffer )
err = emailTpl . Execute ( buf , data )
if err != nil {
return err
}
m . body = buf . String ( )
return nil
}
2020-06-01 19:48:13 +02:00
// MuxSecAllowedDomainsHandler is a security middleware which controls allowed domains.
func MuxSecAllowedDomainsHandler ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
allowedDomains := strings . Split ( viper . GetString ( "ALLOWED_ORIGINS" ) , "," )
allowedOrigins := make ( map [ string ] bool )
for _ , domain := range allowedDomains {
domainTrimmed := strings . TrimSpace ( domain )
allowedOrigins [ fmt . Sprintf ( "http://%s" , domainTrimmed ) ] = true
allowedOrigins [ fmt . Sprintf ( "https://%s" , domainTrimmed ) ] = true
allowedOrigins [ fmt . Sprintf ( "http://www.%s" , domainTrimmed ) ] = true
allowedOrigins [ fmt . Sprintf ( "https://www.%s" , domainTrimmed ) ] = true
}
2020-05-10 00:02:18 +02:00
2020-06-01 19:48:13 +02:00
if len ( r . Header [ "Origin" ] ) == 0 || len ( r . Header [ "Referer" ] ) == 0 {
rawHeader , _ := json . Marshal ( r . Header )
2021-09-19 22:49:48 +02:00
log . Println ( "request with unexpected headers" , string ( rawHeader ) )
2020-06-01 19:48:13 +02:00
w . WriteHeader ( http . StatusForbidden )
return
}
2020-05-10 00:02:18 +02:00
2020-06-01 19:48:13 +02:00
reqOrigin := r . Header [ "Origin" ] [ 0 ]
if _ , domainFound := allowedOrigins [ reqOrigin ] ; ! domainFound {
2021-09-19 22:49:48 +02:00
log . Println ( "not allowed origin" , reqOrigin )
2020-06-01 19:48:13 +02:00
w . WriteHeader ( http . StatusForbidden )
return
}
2022-04-28 23:41:39 +02:00
// Send the allowed origin back as CORS header
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , r . Header [ "Origin" ] [ 0 ] )
2020-06-01 19:48:13 +02:00
next . ServeHTTP ( w , r )
} )
}
2020-05-10 00:02:18 +02:00
2020-06-02 15:27:08 +02:00
// MuxSecReCaptchaHandler is a security middleware which verifies the challenge code from
// the reCaptcha human verification system (provided by Google).
func MuxSecReCaptchaHandler ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
recaptchaResponse , found := r . Form [ "g-recaptcha-response" ]
if found {
remoteIp , _ , _ := net . SplitHostPort ( r . RemoteAddr )
recaptchaPrivateKey := viper . GetString ( "RECAPTCHA_PRIVATE_KEY" )
recaptcha . Init ( recaptchaPrivateKey )
result , err := recaptcha . Confirm ( remoteIp , recaptchaResponse [ 0 ] )
if err != nil {
2021-09-19 22:49:48 +02:00
log . Println ( "reCaptcha server error:" , err . Error ( ) )
2020-06-02 15:27:08 +02:00
w . WriteHeader ( http . StatusForbidden )
return
}
if ! result {
w . WriteHeader ( http . StatusForbidden )
return
}
}
next . ServeHTTP ( w , r )
} )
}
2020-05-10 00:02:18 +02:00
2020-06-01 19:48:13 +02:00
// SendMail handles HTTP request to send email
func SendMail ( httpResp http . ResponseWriter , httpReq * http . Request ) {
2020-05-10 00:02:18 +02:00
httpReq . ParseForm ( )
contactRequest := ContactRequest {
Name : httpReq . FormValue ( "name" ) ,
Email : strings . TrimSpace ( httpReq . FormValue ( "email" ) ) ,
Organization : httpReq . FormValue ( "organization" ) ,
Subject : httpReq . FormValue ( "subject" ) ,
Message : httpReq . FormValue ( "message" ) ,
RequestTarget : httpReq . FormValue ( "target" ) ,
2021-10-26 20:14:24 +02:00
OriginURI : httpReq . FormValue ( "requestOrigin" ) ,
2020-05-10 00:02:18 +02:00
}
var recipients [ ] string
switch contactRequest . RequestTarget {
case "demo" :
2020-05-17 21:53:56 +02:00
recipients = [ ] string { contactRequest . Email , viper . GetString ( "CONTACT_REPLY_BCC_EMAIL" ) }
2020-05-10 00:02:18 +02:00
case "contact" :
2020-05-17 21:53:56 +02:00
recipients = [ ] string { viper . GetString ( "CONTACT_REPLY_BCC_EMAIL" ) }
2020-05-10 00:02:18 +02:00
default :
2021-09-19 22:49:48 +02:00
log . Println ( "not allowed request type:" , contactRequest . RequestTarget )
2020-05-10 00:02:18 +02:00
httpResp . WriteHeader ( http . StatusForbidden )
2021-09-19 22:49:48 +02:00
httpResp . Write ( [ ] byte ( ` { "status": "error", "message": "unauthorized request"} ` ) )
2020-05-10 00:02:18 +02:00
return
}
userData , _ := json . Marshal ( contactRequest )
2021-09-19 22:49:48 +02:00
log . Println ( "New Request:" , string ( userData ) )
2020-05-10 00:02:18 +02:00
templateData := struct {
Name string
Email string
Organization string
Subject string
Message string
DemoURL string
} {
Name : contactRequest . Name ,
Email : contactRequest . Email ,
Organization : contactRequest . Organization ,
Subject : contactRequest . Subject ,
Message : contactRequest . Message ,
DemoURL : viper . GetString ( "DEMO_URL" ) ,
}
2020-05-23 23:25:12 +02:00
replyTplFile := ""
2020-05-10 00:02:18 +02:00
if contactRequest . RequestTarget == "demo" {
2020-06-02 15:27:08 +02:00
replyTplFile = viper . GetString ( "TEMPLATE_DEMO_REQUEST_REPLY" )
2020-05-23 23:25:12 +02:00
if replyTplFile == "" {
replyTplFile = "./templates/template_reply_demo_request.html"
}
2020-05-10 00:02:18 +02:00
} else {
2020-06-02 15:27:08 +02:00
replyTplFile = viper . GetString ( "TEMPLATE_CONTACT_REQUEST_REPLY" )
2020-05-23 23:25:12 +02:00
if replyTplFile == "" {
replyTplFile = "./templates/template_reply_contact_request.html"
}
2020-05-10 00:02:18 +02:00
}
2020-08-20 09:43:14 +02:00
2021-10-26 20:14:24 +02:00
contactEmail := viper . GetString ( "CONTACT_REPLY_EMAIL" )
2020-08-20 09:43:14 +02:00
sendMailReq := NewSendMailRequest (
contactEmail ,
recipients ,
contactRequest . Subject ,
)
2020-05-23 23:25:12 +02:00
err := sendMailReq . ParseTemplate ( replyTplFile , templateData )
2020-05-10 00:02:18 +02:00
2020-08-20 09:43:14 +02:00
contactResponse := ContactResponse { }
2020-05-10 00:02:18 +02:00
if err == nil {
err := sendMailReq . Execute ( )
if err != nil {
2021-09-19 22:49:48 +02:00
log . Println ( err . Error ( ) )
2020-05-10 00:02:18 +02:00
contactResponse . Status = "error"
2020-05-17 21:53:56 +02:00
contactResponse . Message = fmt . Sprintf ( "An internal error occurred, please try later or send us an email at %s." , viper . GetString ( "CONTACT_REPLY_BCC_EMAIL" ) )
2020-05-10 00:02:18 +02:00
} else {
contactResponse . Status = "success"
if contactRequest . RequestTarget == "demo" {
contactResponse . Message = "Thank you, if you supplied a correct email address then an email should have been sent to you."
} else {
contactResponse . Message = "Thank you, if you supplied a correct email address then we'll process your request within the next 48 hours."
}
}
} else {
2021-09-19 22:49:48 +02:00
log . Println ( err . Error ( ) )
2020-05-10 00:02:18 +02:00
contactResponse . Status = "error"
contactResponse . Message = "Invalid request, please review your input and try again."
}
2021-10-26 20:14:24 +02:00
originURL , err := url . Parse ( contactRequest . OriginURI )
2020-06-02 11:36:33 +02:00
if err != nil {
2021-10-26 20:14:24 +02:00
log . Printf ( "error parsing the origin URL %s (%s)" , originURL , err . Error ( ) )
originURL = & url . URL { } // continue with default (empty) url
2020-06-02 11:36:33 +02:00
}
2021-10-26 20:14:24 +02:00
q := originURL . Query ( )
2020-06-02 11:36:33 +02:00
q . Set ( "status" , contactResponse . Status )
q . Set ( "message" , contactResponse . Message )
2021-10-26 20:14:24 +02:00
originURL . RawQuery = q . Encode ( )
2020-06-02 11:36:33 +02:00
2020-05-10 00:02:18 +02:00
respRawData , _ := json . Marshal ( contactResponse )
2021-10-26 20:14:24 +02:00
httpResp . Header ( ) . Set ( "Location" , originURL . String ( ) )
2020-05-10 00:02:18 +02:00
httpResp . WriteHeader ( http . StatusSeeOther )
httpResp . Header ( ) . Set ( "Content-Type" , "application/json; charset=UTF-8" )
httpResp . Write ( respRawData )
}