From 074feb0f8d95d065c0f5aa38213ff02b472233e7 Mon Sep 17 00:00:00 2001 From: rainerosion <1782832653@qq.com> Date: Sat, 15 Apr 2023 15:09:10 +0800 Subject: [PATCH] init --- .gitignore | 1 + .idea/.gitignore | 8 +++ Dockerfile | 9 +++ Makefile | 3 + README.md | 39 ++++++++++++ bootstrap/bootstrap.go | 34 ++++++++++ config.json.example | 8 +++ config/config.go | 64 +++++++++++++++++++ go.mod | 5 ++ go.sum | 2 + gpt/bing.go | 92 +++++++++++++++++++++++++++ gpt/bing_test.go | 15 +++++ gpt/gpt.go | 113 ++++++++++++++++++++++++++++++++++ gpt/gpt_test.go | 14 +++++ handlers/group_msg_handler.go | 95 ++++++++++++++++++++++++++++ handlers/handler.go | 52 ++++++++++++++++ handlers/user_msg_handler.go | 78 +++++++++++++++++++++++ main.go | 9 +++ start.sh | 9 +++ utils/send_mail.go | 37 +++++++++++ 20 files changed, 687 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 bootstrap/bootstrap.go create mode 100644 config.json.example create mode 100644 config/config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gpt/bing.go create mode 100644 gpt/bing_test.go create mode 100644 gpt/gpt.go create mode 100644 gpt/gpt_test.go create mode 100644 handlers/group_msg_handler.go create mode 100644 handlers/handler.go create mode 100644 handlers/user_msg_handler.go create mode 100644 main.go create mode 100644 start.sh create mode 100644 utils/send_mail.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4983c0e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +storage.json diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..60937b7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.16 as builder +ENV GOPROXY=https://goproxy.cn,direct +WORKDIR /app +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o chatgpt +FROM alpine:3.17.2 +WORKDIR /app +COPY --from=builder /app/chatgpt . +ENTRYPOINT ["./chatgpt"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a3cb9a3 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +build: + docker build -t yy194131/chatgpt:$(version) . + docker push yy194131/chatgpt:$(version) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6268232 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +## 微信机器人 + +最近ChatGPT异常火爆,想到将其接入到个人微信是件比较有趣的事,所以有了这个项目。项目基于[openwechat](https://github.com/eatmoreapple/openwechat)开发,支持ChatGPT和New Bing,其中New Bing[依赖于new-bing项目](../new-bing)提供的http接口。 + +## 目前实现了以下功能 + + 群聊@回复 + + 私聊回复 + + 自动通过回复 + +## 注册openai +ChatGPT注册可以参考[这里](https://juejin.cn/post/7173447848292253704) + +## 部署 + +``` +# 获取项目 +git clone https://github.com/bujnlc8/gptbing + +# 进入项目目录 +cd gptbing/wechatbot + +# 复制配置文件, 改成实际的值,注意去掉#注释 +copy config.json.example config.json + +# 启动项目 +go run main.go + + +或者docker部署 + +bash start.sh 0.0.2 # 代理地址根据实际情况修改,运行之后`docker logs wechatbot`会打印出登录地址 + +``` + +## 说明 + +本项目fork至[https://github.com/djun/wechatbot](https://github.com/djun/wechatbot),修改使之支持`ChatGPT`和`New Bing`,在此致谢! + +**⚠️ 有一定的几率导致被微信封号,请谨慎使用,由此导致的封号,本人概不负责** diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go new file mode 100644 index 0000000..b7ea463 --- /dev/null +++ b/bootstrap/bootstrap.go @@ -0,0 +1,34 @@ +package bootstrap + +import ( + "log" + + "github.com/bujnlc8/wechatbot/handlers" + "github.com/bujnlc8/wechatbot/utils" + "github.com/eatmoreapple/openwechat" +) + +func Run() { + //bot := openwechat.DefaultBot() + bot := openwechat.DefaultBot(openwechat.Desktop) // 桌面模式,上面登录不上的可以尝试切换这种模式 + + // 注册消息处理函数 + bot.MessageHandler = handlers.Handler + // 注册登陆二维码回调 + bot.UUIDCallback = openwechat.PrintlnQrcodeUrl + + // 创建热存储容器对象 + reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json") + // 执行热登录 + err := bot.HotLogin(reloadStorage) + if err != nil { + if err = bot.Login(); err != nil { + log.Printf("login error: %v \n", err) + return + } + } + // 阻塞主goroutine, 直到发生异常或者用户主动退出 + if err := bot.Block(); err != nil { + utils.SendSimpleEmail("[!]wechatbot异常退出", err.Error()) + } +} diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..f525071 --- /dev/null +++ b/config.json.example @@ -0,0 +1,8 @@ +{ + "api_key": "", # chatgpt api key + "auto_pass": true, # 是否自动通过好友申请 + "bing_chat_url": "" # New Bing 聊天接口 + "bing_chat_wake_word": "#bing", # new Bing唤醒词 + "gpt_chat_wake_word": "#gpt" # ChatGPT唤醒词 + "gpt_message_cache": 1 # 消息缓存数量 +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..d24c9ba --- /dev/null +++ b/config/config.go @@ -0,0 +1,64 @@ +package config + +import ( + "encoding/json" + "log" + "os" + "sync" +) + +// Configuration 项目配置 +type Configuration struct { + // gpt apikey + ApiKey string `json:"api_key"` + // 自动通过好友 + AutoPass bool `json:"auto_pass"` + // bing 聊天接口 + BingChatUrl string `json:"bing_chat_url"` + GptChatUrl string `json:"gpt_chat_url"` + // 机器人唤醒词 + BingChatWakeWord string `json:"bing_chat_wake_word"` + GptChatWakeWord string `json:"gpt_chat_wake_word"` + // 消息缓存数量 + GptMessageCache int `json:"gpt_message_cache"` + // gpt上下文清空指令 + GptCleanContext string `json:"gpt_clean_context"` +} + +var config *Configuration +var once sync.Once + +// LoadConfig 加载配置 +func LoadConfig() *Configuration { + once.Do(func() { + // 从文件中读取 + config = &Configuration{} + f, err := os.Open("config.json") + if err != nil { + log.Fatalf("open config err: %v", err) + return + } + defer f.Close() + encoder := json.NewDecoder(f) + err = encoder.Decode(config) + if err != nil { + log.Fatalf("decode config err: %v", err) + return + } + + // 如果环境变量有配置,读取环境变量 + ApiKey := os.Getenv("ApiKey") + AutoPass := os.Getenv("AutoPass") + BingChatUrl := os.Getenv("BingChatUrl") + if ApiKey != "" { + config.ApiKey = ApiKey + } + if AutoPass == "true" { + config.AutoPass = true + } + if BingChatUrl != "" { + config.BingChatUrl = BingChatUrl + } + }) + return config +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2563054 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/bujnlc8/wechatbot + +go 1.16 + +require github.com/eatmoreapple/openwechat v1.4.2-0.20230321053318-1c102bdea8d7 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d2750a9 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/eatmoreapple/openwechat v1.4.2-0.20230321053318-1c102bdea8d7 h1:5vryo6dsmZZn1pYo8TrcqEUJq075QMpdDwLZz+oMfLs= +github.com/eatmoreapple/openwechat v1.4.2-0.20230321053318-1c102bdea8d7/go.mod h1:ZxMcq7IpVWVU9JG7ERjExnm5M8/AQ6yZTtX30K3rwRQ= diff --git a/gpt/bing.go b/gpt/bing.go new file mode 100644 index 0000000..963b5ca --- /dev/null +++ b/gpt/bing.go @@ -0,0 +1,92 @@ +package gpt + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + + "github.com/bujnlc8/wechatbot/config" +) + +const Referer = "https://servicewechat.com/wxee7496be5b68b740" + +type BingQueryParam struct { + Q string `json:"q"` + SID string `json:"sid"` + AutoReset string `json:"auto_reset"` +} + +type BingResponse struct { + Data BingResponseData `json:"data"` + Cookie string `json:"cookie"` +} + +type BingResponseData struct { + Suggests []string `json:"suggests"` + Status string `json:"status"` + Text string `json:"text"` + Message string `json:"message"` +} + +func BingSearch(msg string, nickName string) (string, error) { + requestBody := BingQueryParam{ + Q: msg, + SID: nickName, + AutoReset: "1", + } + requestData, err := json.Marshal(requestBody) + + if err != nil { + return "", err + } + log.Printf("request bing json string : %v", string(requestData)) + BingChatUrl := config.LoadConfig().BingChatUrl + req, err := http.NewRequest("POST", BingChatUrl, bytes.NewBuffer(requestData)) + if err != nil { + return "", err + } + req.Header.Set("Referer", Referer) + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + response, err := client.Do(req) + if err != nil { + return "非常抱歉😭,网络异常,请稍后重试", err + } + if response.StatusCode != 200 { + return "非常抱歉😭,网络异常,请稍后重试 [" + strconv.Itoa(response.StatusCode) + "]", nil + } + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return "响应异常,请稍后再试", err + } + + bingResponse := &BingResponse{} + log.Println(string(body)) + err = json.Unmarshal(body, bingResponse) + if err != nil { + return "", err + } + if bingResponse.Data.Status == "Success" { + if strings.Contains(bingResponse.Data.Text, "New topic") { + return bingResponse.Data.Text + "\n请重新开始对话", nil + } + return bingResponse.Data.Text, nil + + } else { + if bingResponse.Data.Status == "Throttled" { + return "这真是愉快,但你已达到每日限制。是否明天再聊?", nil + } else { + if strings.Contains(bingResponse.Data.Message, "has expired") { + return "本轮对话已过期,请重新开始。", nil + } else { + return "抱歉😭,发生错误:" + bingResponse.Data.Message + ",请重试", nil + } + } + } +} diff --git a/gpt/bing_test.go b/gpt/bing_test.go new file mode 100644 index 0000000..2b59c2b --- /dev/null +++ b/gpt/bing_test.go @@ -0,0 +1,15 @@ +package gpt + +import ( + "fmt" + "testing" +) + +func TestBing(t *testing.T) { + reply, err := BingSearch("今天北京的天气怎么样", "nickname") + if err != nil{ + t.Error(err) + } + fmt.Printf("%+v\n", reply) + +} diff --git a/gpt/gpt.go b/gpt/gpt.go new file mode 100644 index 0000000..9755cae --- /dev/null +++ b/gpt/gpt.go @@ -0,0 +1,113 @@ +package gpt + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "log" + "net/http" + + "github.com/bujnlc8/wechatbot/config" +) + +var BASEURL = config.LoadConfig().GptChatUrl + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// ChatGPTResponseBody 请求体 +type ChatGPTResponseBody struct { + ID string `json:"id"` + Object string `json:"object"` + Created int `json:"created"` + Model string `json:"model"` + Choices []ChoiceItem `json:"choices"` + Usage map[string]interface{} `json:"usage"` +} + +type ChoiceItem struct { + Message Message `json:"message"` + FinishReason string `json:"finish_reason"` +} + +// ChatGPTRequestBody 响应体 +type ChatGPTRequestBody struct { + Model string `json:"model"` + MaxTokens int `json:"max_tokens"` + Temperature float32 `json:"temperature"` + Messages []Message `json:"messages"` +} + +var MessageCacheRegistry = make(map[string][]Message) + +func CleanContext(nickName string) (string, error) { + messageCache := []Message{} + MessageCacheRegistry[nickName] = messageCache + return "用户[" + nickName + "]上下文已清空", nil +} + +func Completions(msg string, nickName string) (string, error) { + cache := config.LoadConfig().GptMessageCache + messageCache := MessageCacheRegistry[nickName] + message := Message{Role: "user", Content: msg} + if messageCache == nil || len(messageCache) == 0 { + messageCache = []Message{message} + } else { + messageCache = append(messageCache, message) + // 只保留20条 + if len(messageCache) > cache { + messageCache = messageCache[(len(messageCache) - cache):] + } + } + MessageCacheRegistry[nickName] = messageCache + requestBody := ChatGPTRequestBody{ + Model: "gpt-3.5-turbo", + MaxTokens: 4096, + Temperature: 1.2, + Messages: messageCache, + } + requestData, err := json.Marshal(requestBody) + + if err != nil { + return "", err + } + log.Printf("request gpt json string : %v", string(requestData)) + req, err := http.NewRequest("POST", BASEURL+"completions", bytes.NewBuffer(requestData)) + if err != nil { + return "", err + } + + apiKey := config.LoadConfig().ApiKey + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + client := &http.Client{} + response, err := client.Do(req) + if err != nil { + return "", err + } + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", err + } + gptResponseBody := &ChatGPTResponseBody{} + log.Println(string(body)) + err = json.Unmarshal(body, gptResponseBody) + if err != nil { + return "", err + } + var reply string + if len(gptResponseBody.Choices) > 0 { + for _, v := range gptResponseBody.Choices { + messageCache = append(messageCache, v.Message) + MessageCacheRegistry[nickName] = messageCache + reply = v.Message.Content + break + } + } + log.Printf("gpt response text: %s \n", reply) + return reply, nil +} diff --git a/gpt/gpt_test.go b/gpt/gpt_test.go new file mode 100644 index 0000000..e8ec475 --- /dev/null +++ b/gpt/gpt_test.go @@ -0,0 +1,14 @@ +package gpt + +import ( + "fmt" + "testing" +) + +func TestGpt(t *testing.T) { + reply, err := Completions("今天北京的天气怎么样", "nickname") + if err != nil { + t.Error(err) + } + fmt.Printf("%+v\n", reply) +} diff --git a/handlers/group_msg_handler.go b/handlers/group_msg_handler.go new file mode 100644 index 0000000..200de80 --- /dev/null +++ b/handlers/group_msg_handler.go @@ -0,0 +1,95 @@ +package handlers + +import ( + "github.com/bujnlc8/wechatbot/config" + "log" + "strings" + + "github.com/bujnlc8/wechatbot/gpt" + "github.com/eatmoreapple/openwechat" +) + +var _ MessageHandlerInterface = (*GroupMessageHandler)(nil) + +// GroupMessageHandler 群消息处理 +type GroupMessageHandler struct { +} + +// handle 处理消息 +func (g *GroupMessageHandler) handle(msg *openwechat.Message) error { + bingWakeWord := config.LoadConfig().BingChatWakeWord + gptWakeWord := config.LoadConfig().GptChatWakeWord + if msg.IsText() { + return g.ReplyText(msg) + } else { + if strings.Contains(msg.Content, gptWakeWord) || strings.Contains(msg.Content, bingWakeWord) { + msg.ReplyText("目前我只支持文字哦~") + } + } + if msg.IsPaiYiPai() { + msg.ReplyText("我是机器人🤖️,会拍坏的哦~") + } + return nil +} + +// NewGroupMessageHandler 创建群消息处理器 +func NewGroupMessageHandler() MessageHandlerInterface { + return &GroupMessageHandler{} +} + +// ReplyText 发送文本消息到群 +func (g *GroupMessageHandler) ReplyText(msg *openwechat.Message) error { + sender, err := msg.Sender() + group := openwechat.Group{User: sender} + log.Printf("Received Group %v Text Msg : %v", group.NickName, msg.Content) + + bingWakeWord := config.LoadConfig().BingChatWakeWord + gptWakeWord := config.LoadConfig().GptChatWakeWord + + // @GPTBot 或者 @bing的消息才处理 + if !(strings.Contains(msg.Content, gptWakeWord) || strings.Contains(msg.Content, bingWakeWord)) { + return nil + } + + requestText := strings.TrimSpace(strings.ReplaceAll(msg.Content, gptWakeWord, "")) + var reply = "" + if strings.EqualFold(msg.Content, config.LoadConfig().GptCleanContext) { + cleanReply, _ := gpt.CleanContext(group.UserName) + reply = "\n" + cleanReply + } else if strings.Contains(msg.Content, bingWakeWord) { + requestText = strings.TrimSpace(strings.ReplaceAll(msg.Content, bingWakeWord, "")) + reply, err = gpt.BingSearch(requestText, group.UserName) + if reply != "" && strings.HasPrefix(reply, "[") { + reply = "\n" + reply + } + } else { + reply, err = gpt.Completions(requestText, group.UserName) + } + if err != nil { + log.Printf("gpt request error: %v \n", err) + msg.ReplyText("机器人神了,我一会发现了就去修。") + return err + } + if reply == "" { + msg.ReplyText("机器人响应为空") + return nil + } + + // 获取@我的用户 + groupSender, err := msg.SenderInGroup() + if err != nil { + log.Printf("get sender in group error :%v \n", err) + return err + } + + // 回复@我的用户 + reply = strings.TrimSpace(reply) + reply = strings.Trim(reply, "\n") + atText := "@" + groupSender.NickName + replyText := atText + " " + reply + _, err = msg.ReplyText(replyText) + if err != nil { + log.Printf("response group error: %v \n", err) + } + return err +} diff --git a/handlers/handler.go b/handlers/handler.go new file mode 100644 index 0000000..1423b00 --- /dev/null +++ b/handlers/handler.go @@ -0,0 +1,52 @@ +package handlers + +import ( + "log" + + "github.com/bujnlc8/wechatbot/config" + "github.com/eatmoreapple/openwechat" +) + +// MessageHandlerInterface 消息处理接口 +type MessageHandlerInterface interface { + handle(*openwechat.Message) error + ReplyText(*openwechat.Message) error +} + +type HandlerType string + +const ( + GroupHandler = "group" + UserHandler = "user" +) + +// handlers 所有消息类型类型的处理器 +var handlers map[HandlerType]MessageHandlerInterface + +func init() { + handlers = make(map[HandlerType]MessageHandlerInterface) + handlers[GroupHandler] = NewGroupMessageHandler() + handlers[UserHandler] = NewUserMessageHandler() +} + +// Handler 全局处理入口 +func Handler(msg *openwechat.Message) { + log.Printf("hadler Received msg : %v", msg.Content) + // 处理群消息 + if msg.IsSendByGroup() { + handlers[GroupHandler].handle(msg) + return + } + // 好友申请 + if msg.IsFriendAdd() { + if config.LoadConfig().AutoPass { + _, err := msg.Agree("你好我是基于chatGPT引擎开发的微信机器人,你可以向我提问任何问题。") + if err != nil { + log.Fatalf("add friend agree error : %v", err) + return + } + } + } + // 私聊 + handlers[UserHandler].handle(msg) +} diff --git a/handlers/user_msg_handler.go b/handlers/user_msg_handler.go new file mode 100644 index 0000000..e078dda --- /dev/null +++ b/handlers/user_msg_handler.go @@ -0,0 +1,78 @@ +package handlers + +import ( + "github.com/bujnlc8/wechatbot/config" + "log" + "strings" + + "github.com/bujnlc8/wechatbot/gpt" + "github.com/eatmoreapple/openwechat" +) + +var _ MessageHandlerInterface = (*UserMessageHandler)(nil) + +// UserMessageHandler 私聊消息处理 +type UserMessageHandler struct { +} + +// handle 处理消息 +func (g *UserMessageHandler) handle(msg *openwechat.Message) error { + if msg.IsText() { + return g.ReplyText(msg) + } + if msg.IsSendByFriend() { + if msg.IsPaiYiPai() { + msg.ReplyText("我是机器人🤖️,会拍坏的哦~") + } else { + msg.ReplyText("目前我只支持文字哦~") + } + } + return nil +} + +// NewUserMessageHandler 创建私聊处理器 +func NewUserMessageHandler() MessageHandlerInterface { + return &UserMessageHandler{} +} + +// ReplyText 发送文本消息到群 +func (g *UserMessageHandler) ReplyText(msg *openwechat.Message) error { + // 接收私聊消息 + sender, err := msg.Sender() + log.Printf("Received User %v Text Msg : %v", sender.UserName, msg.Content) + + requestText := strings.TrimSpace(msg.Content) + requestText = strings.Trim(msg.Content, "\n") + + bingWakeWord := config.LoadConfig().BingChatWakeWord + gptWakeWord := config.LoadConfig().GptChatWakeWord + + var reply = "" + if strings.Contains(msg.Content, bingWakeWord) { + requestText = strings.TrimSpace(strings.ReplaceAll(msg.Content, bingWakeWord, "")) + reply, err = gpt.BingSearch(requestText, sender.UserName) + } else if strings.Contains(msg.Content, gptWakeWord) { + requestText = strings.TrimSpace(strings.ReplaceAll(msg.Content, gptWakeWord, "")) + reply, err = gpt.Completions(requestText, sender.UserName) + } else { + //可以考虑对接其他机器人恢复信息 + } + if err != nil { + log.Printf("gpt request error: %v \n", err) + msg.ReplyText("机器人神了,我一会发现了就去修。") + return err + } + if reply == "" { + msg.ReplyText("机器人响应为空") + return nil + } + + // 回复用户 + reply = strings.TrimSpace(reply) + reply = strings.Trim(reply, "\n") + _, err = msg.ReplyText(reply) + if err != nil { + log.Printf("response user error: %v \n", err) + } + return err +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4ae64b0 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/bujnlc8/wechatbot/bootstrap" +) + +func main() { + bootstrap.Run() +} diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..b626a52 --- /dev/null +++ b/start.sh @@ -0,0 +1,9 @@ +docker pull yy194131/chatgpt:$1 + +count=$(docker ps -a | grep wechatbot | wc -l) +if [ $count -gt 0 ]; then + docker stop wechatbot && docker rm wechatbot +fi + +# 代理地址改成实际的地址 +docker run --name wechatbot -d -e https_proxy=代理地址 -v $(pwd)/config.json:/app/config.json yy194131/chatgpt:$1 diff --git a/utils/send_mail.go b/utils/send_mail.go new file mode 100644 index 0000000..a4e57a5 --- /dev/null +++ b/utils/send_mail.go @@ -0,0 +1,37 @@ +package utils + +import ( + "log" + "net/smtp" + "os" +) + +const ( + SMTP_SERVER = "smtp.qq.com" + SMTP_PORT = "587" +) + +// 用qq邮箱发送邮件,从New Bing写的代码修改而来 +func SendSimpleEmail(subject string, body string) { + sender := os.Getenv("EMAIL_SENDER") + passwd := os.Getenv("EMAIL_PASSWD") + recipient := os.Getenv("EMAIL_RECIPIENT") + if len(recipient) == 0 { + recipient = sender + } + auth := smtp.PlainAuth("", sender, passwd, SMTP_SERVER) + header := make(map[string]string) + header["To"] = recipient + header["From"] = sender + header["Subject"] = subject + headerStr := "" + for k, v := range header { + headerStr += k + ": " + v + "\r\n" + } + message := headerStr + "\r\n\r\n" + body + err := smtp.SendMail(SMTP_SERVER+":"+SMTP_PORT, auth, sender, []string{recipient}, []byte(message)) + if err != nil { + log.Fatal(err) + } + log.Println("Email sent successfully") +}