├── .gitignore ├── app.dev.yaml ├── deploy.sh ├── app.yaml ├── common ├── utils │ └── utils.go ├── db │ ├── client.go │ └── entity.go └── key │ └── key.go ├── register.sh ├── app ├── app.go ├── context.go └── api.go ├── conf ├── conf.go └── build.go ├── main.go ├── handler ├── template │ └── common.go ├── endpoint │ ├── key.go │ ├── receiver │ │ └── common.go │ ├── preference │ │ └── common.go │ └── common.go ├── user │ └── common.go ├── routes.go ├── mail │ ├── list.go │ ├── common.go │ ├── send.go │ └── sender.go ├── dialer │ └── common.go ├── userapp │ └── common.go └── shortcut │ └── shortcut.go ├── log └── log.go ├── middleware ├── auth │ └── guard.go └── cors │ └── cors.go ├── gomailer.sql ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | backupfile 3 | My.json -------------------------------------------------------------------------------- /app.dev.yaml: -------------------------------------------------------------------------------- 1 | env: dev 2 | app: 3 | port: 6060 4 | 5 | data-source: 6 | url: root:xxx@tcp(127.0.0.1:3306)/gomailer -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | nohup go run main.go > /root/go/src/GoMailer/server.log 2>&1 & 2 | nohup ./GoMailer > server.log 2>&1 & 3 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | env: prod 2 | app: 3 | port: 6060 4 | 5 | data-source: 6 | url: root:xxx@tcp(127.0.0.1:3306)/gomailer 7 | 8 | re-captcha-secret: xxxxxxxxx -------------------------------------------------------------------------------- /common/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | func IsBlankStr(str string) bool { 6 | return len(strings.TrimSpace(str)) == 0 7 | } 8 | -------------------------------------------------------------------------------- /register.sh: -------------------------------------------------------------------------------- 1 | function registerApp() 2 | { 3 | echo "Sending request... $1" 4 | info=`curl -H "Content-Type:application/json" -X POST --data @$2 $1` 5 | echo $info 6 | } 7 | 8 | registerApp $1 $2 -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "GoMailer/conf" 9 | ) 10 | 11 | func IsDevAppServer() bool { 12 | return conf.Env() == "dev" 13 | } 14 | 15 | func JsonUnmarshalFromRequest(r *http.Request, dst interface{}) *Error { 16 | all, err := ioutil.ReadAll(r.Body) 17 | if err != nil { 18 | return Errorf(err, "failed to parse request body") 19 | } 20 | 21 | err = json.Unmarshal(all, dst) 22 | if err != nil { 23 | return Errorf(err, "failed to parse request body") 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type config struct { 4 | Env string `yaml:"env"` 5 | App *app `yaml:"app"` 6 | DataSource *dataSource `yaml:"data-source"` 7 | 8 | ReCaptchaSecret string `yaml:"re-captcha-secret"` 9 | } 10 | 11 | type dataSource struct { 12 | URL string `yaml:"url"` 13 | } 14 | 15 | type app struct { 16 | Port string `yaml:"port"` 17 | } 18 | 19 | var ( 20 | conf *config 21 | ) 22 | 23 | func DataSource() *dataSource { 24 | return conf.DataSource 25 | } 26 | 27 | func Env() string { 28 | return conf.Env 29 | } 30 | 31 | func ReCaptchaSecret() string { 32 | return conf.ReCaptchaSecret 33 | } 34 | 35 | func App() *app { 36 | return conf.App 37 | } 38 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "GoMailer/app" 7 | "GoMailer/conf" 8 | "GoMailer/handler" 9 | _ "GoMailer/handler/mail" 10 | _ "GoMailer/handler/shortcut" 11 | "GoMailer/log" 12 | ) 13 | 14 | func init() { 15 | http.Handle("/api/", handler.Router) 16 | } 17 | 18 | func main() { 19 | port := "8080" 20 | if s := conf.App().Port; s != "" { 21 | port = s 22 | } 23 | 24 | host := "" 25 | if app.IsDevAppServer() { 26 | host = "127.0.0.1" 27 | } 28 | 29 | addr := host + ":" + port 30 | log.Infof("server start at %s with env: %s", addr, conf.Env()) 31 | if err := http.ListenAndServe(addr, app.RootHandler()); err != nil { 32 | log.Fatalf("http.ListenAndServe: %v", err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /conf/build.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "GoMailer/log" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | func init() { 13 | path := getConfig() 14 | log.Info("parse config file at: ", path) 15 | 16 | conf = &config{} 17 | if f, err := os.Open(path); err != nil { 18 | panic(err) 19 | } else { 20 | if err = yaml.NewDecoder(f).Decode(conf); err != nil { 21 | panic(err) 22 | } 23 | } 24 | } 25 | 26 | func getConfig() string { 27 | defaultConfig := "app.yaml" 28 | 29 | if len(os.Args) == 2 { 30 | env := os.Args[1] 31 | if len(strings.TrimSpace(env)) == 0 { 32 | return defaultConfig 33 | } 34 | 35 | return fmt.Sprintf("app.%s.yaml", env) 36 | } 37 | 38 | return defaultConfig 39 | } 40 | -------------------------------------------------------------------------------- /handler/template/common.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "errors" 5 | 6 | "GoMailer/common/db" 7 | "GoMailer/common/utils" 8 | ) 9 | 10 | func Create(t *db.Template) (*db.Template, error) { 11 | if utils.IsBlankStr(t.Template) { 12 | return nil, errors.New("template can not be empty") 13 | } 14 | 15 | client, err := db.NewClient() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | userExist, err := client.ID(t.UserId).Exist(&db.User{}) 21 | if err != nil { 22 | return nil, err 23 | } 24 | if !userExist { 25 | return nil, errors.New("user not exist") 26 | } 27 | 28 | affected, err := client.InsertOne(t) 29 | if err != nil { 30 | return nil, err 31 | } 32 | if affected != 1 { 33 | return nil, errors.New("failed to InsertOne user app") 34 | } 35 | 36 | return t, nil 37 | } 38 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | func Info(args ...interface{}) { 9 | log.Println("INFO: " + fmt.Sprint(args...)) 10 | } 11 | 12 | func Infof(format string, args ...interface{}) { 13 | log.Println("INFO: " + fmt.Sprintf(format, args...)) 14 | } 15 | 16 | func Warning(args ...interface{}) { 17 | log.Println("WARN: " + fmt.Sprint(args...)) 18 | } 19 | 20 | func Warningf(format string, args ...interface{}) { 21 | log.Println("WARN: " + fmt.Sprintf(format, args...)) 22 | } 23 | 24 | func Error(args ...interface{}) { 25 | log.Println("ERROR: " + fmt.Sprint(args...)) 26 | } 27 | 28 | func Errorf(format string, args ...interface{}) { 29 | log.Println("ERROR: " + fmt.Sprintf(format, args...)) 30 | } 31 | func Fatalf(format string, v ...interface{}) { 32 | log.Fatalf(format, v...) 33 | } 34 | -------------------------------------------------------------------------------- /middleware/auth/guard.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | 6 | "GoMailer/common/key" 7 | "GoMailer/common/utils" 8 | "GoMailer/handler/endpoint" 9 | ) 10 | 11 | var ( 12 | noGuardRequiredAPI = map[string]struct{}{ 13 | "/api/shortcut": {}, 14 | } 15 | ) 16 | 17 | func Guard(next http.Handler) http.Handler { 18 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | if _, ok := noGuardRequiredAPI[r.URL.Path]; !ok { 20 | epk := key.EPKeyFromRequest(r) 21 | if utils.IsBlankStr(epk) { 22 | http.Error(w, "EPKey is required", http.StatusUnauthorized) 23 | return 24 | } 25 | ep, err := endpoint.FindByKey(epk) 26 | if err != nil { 27 | http.Error(w, err.Error(), http.StatusUnauthorized) 28 | return 29 | } 30 | if ep == nil { 31 | http.Error(w, "endpoint not exist", http.StatusUnauthorized) 32 | return 33 | } 34 | } 35 | 36 | next.ServeHTTP(w, r) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /handler/endpoint/key.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "errors" 5 | 6 | "GoMailer/common/db" 7 | "GoMailer/common/key" 8 | "GoMailer/common/utils" 9 | ) 10 | 11 | func FindByKey(key string) (*db.Endpoint, error) { 12 | if utils.IsBlankStr(key) { 13 | return nil, nil 14 | } 15 | 16 | client, err := db.NewClient() 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | u := &db.Endpoint{} 22 | has, err := client.Where("`key` = ?", key).Get(u) 23 | if err != nil { 24 | return nil, err 25 | } 26 | if !has { 27 | return nil, nil 28 | } 29 | 30 | return u, nil 31 | } 32 | 33 | func RefreshKey(endpointId int64) (string, error) { 34 | client, err := db.NewClient() 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | ud := &db.Endpoint{Id: endpointId, Key: key.GenerateKey()} 40 | affected, err := client.Id(endpointId).Update(ud) 41 | if err != nil { 42 | return "", err 43 | } 44 | if affected != 1 { 45 | return "", errors.New("failed to Update endpoint key") 46 | } 47 | 48 | return ud.Key, nil 49 | } 50 | -------------------------------------------------------------------------------- /handler/user/common.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | 6 | "GoMailer/common/db" 7 | "GoMailer/common/utils" 8 | ) 9 | 10 | func FindByName(username string) (*db.User, error) { 11 | if utils.IsBlankStr(username) { 12 | return nil, nil 13 | } 14 | 15 | client, err := db.NewClient() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | u := &db.User{} 21 | has, err := client.Where("username = ?", username).Get(u) 22 | if err != nil { 23 | return nil, err 24 | } 25 | if !has { 26 | return nil, nil 27 | } 28 | 29 | return u, nil 30 | } 31 | 32 | func Create(user *db.User) (*db.User, error) { 33 | if utils.IsBlankStr(user.Username) { 34 | return nil, errors.New("username can not be empty") 35 | } 36 | if utils.IsBlankStr(user.Password) || len(user.Password) < 6 { 37 | return nil, errors.New("password invalid, min length is 6") 38 | } 39 | 40 | client, err := db.NewClient() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | one, err := client.InsertOne(user) 46 | if err != nil { 47 | return nil, err 48 | } 49 | if one != 1 { 50 | return nil, errors.New("failed to InsertOne user") 51 | } 52 | 53 | return user, nil 54 | } 55 | -------------------------------------------------------------------------------- /common/db/client.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | _ "github.com/go-sql-driver/mysql" 5 | "github.com/go-xorm/xorm" 6 | "xorm.io/core" 7 | 8 | "GoMailer/app" 9 | "GoMailer/conf" 10 | "GoMailer/log" 11 | ) 12 | 13 | var ( 14 | engine *xorm.Engine 15 | ) 16 | 17 | func NewClient() (*xorm.Engine, error) { 18 | if engine == nil { 19 | err := prepareEngine() 20 | if err != nil { 21 | return nil, err 22 | } 23 | } 24 | 25 | err := engine.Ping() 26 | if err != nil { 27 | err := engine.Close() 28 | if err != nil { 29 | return nil, err 30 | } 31 | log.Warningf("xorm engine closed due to ping failed, will recreate soon.") 32 | 33 | // Recreate engine. 34 | err = prepareEngine() 35 | if err != nil { 36 | return nil, err 37 | } 38 | } 39 | return engine, nil 40 | } 41 | 42 | func prepareEngine() error { 43 | var err error 44 | engine, err = xorm.NewEngine("mysql", conf.DataSource().URL) 45 | if err != nil { 46 | log.Errorf("fail to create db engine: %v", err) 47 | return err 48 | } 49 | err = engine.Ping() 50 | if err != nil { 51 | log.Errorf("fail to ping db: %v", err) 52 | return err 53 | } 54 | 55 | engine.SetMapper(core.SnakeMapper{}) 56 | 57 | engine.ShowSQL(app.IsDevAppServer()) 58 | engine.ShowExecTime(true) 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /handler/routes.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | 6 | "GoMailer/middleware/auth" 7 | "GoMailer/middleware/cors" 8 | ) 9 | 10 | var ( 11 | Router = mux.NewRouter() 12 | APIRouter = Router.PathPrefix("/api/").Subrouter() 13 | 14 | ShortcutRouter = APIRouter.Path("/shortcut").Subrouter() 15 | 16 | // Root properties 17 | // /api/* 18 | MailRouter = APIRouter.PathPrefix("/mail/").Subrouter() 19 | UserRouter = APIRouter.PathPrefix("/user/").Subrouter() 20 | 21 | // User properties. 22 | // /api/user/{UID}/* 23 | DialerRouter = UserRouter.PathPrefix("/{UID}/dialer").Subrouter() 24 | TemplateRouter = UserRouter.PathPrefix("/{UID}/template").Subrouter() 25 | AppRouter = UserRouter.PathPrefix("/{UID}/app").Subrouter() 26 | 27 | // User app properties. 28 | // /api/user/{UID}/app/{AID}/* 29 | EndPointRouter = AppRouter.PathPrefix("/{AID}/endpoint").Subrouter() 30 | 31 | // End point properties. 32 | // /api/user/{UID}/app/{AID}/endpoint/{EID}/* 33 | PreferenceRouter = EndPointRouter.PathPrefix("/{EID}/preference").Subrouter() 34 | ReceiverRouter = EndPointRouter.PathPrefix("/{EID}/receiver").Subrouter() 35 | ) 36 | 37 | func init() { 38 | var middleware []mux.MiddlewareFunc 39 | middleware = append(middleware, cors.CORS(APIRouter)) 40 | middleware = append(middleware, auth.Guard) 41 | APIRouter.Use(middleware...) 42 | } 43 | -------------------------------------------------------------------------------- /handler/endpoint/receiver/common.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "errors" 5 | 6 | "GoMailer/common/db" 7 | "GoMailer/common/utils" 8 | ) 9 | 10 | func DeleteByEndpoint(endpointId int64) error { 11 | client, err := db.NewClient() 12 | if err != nil { 13 | return err 14 | } 15 | 16 | _, err = client.Where("endpoint_id = ?", endpointId).Delete(&db.Receiver{}) 17 | if err != nil { 18 | return err 19 | } 20 | return nil 21 | } 22 | 23 | func PatchCreate(receiver []*db.Receiver) error { 24 | client, err := db.NewClient() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | for _, r := range receiver { 30 | userExist, err := client.ID(r.UserId).Exist(&db.User{}) 31 | if err != nil { 32 | return err 33 | } 34 | if !userExist { 35 | return errors.New("user not exist") 36 | } 37 | userAppExist, err := client.ID(r.UserAppId).Exist(&db.UserApp{}) 38 | if err != nil { 39 | return err 40 | } 41 | if !userAppExist { 42 | return errors.New("user app not exist") 43 | } 44 | endpointExist, err := client.ID(r.EndpointId).Exist(&db.Endpoint{}) 45 | if err != nil { 46 | return err 47 | } 48 | if !endpointExist { 49 | return errors.New("endpoint not exist") 50 | } 51 | if utils.IsBlankStr(r.Address) { 52 | return errors.New("address can not be empty") 53 | } 54 | if db.ReceiverType(r.ReceiverType) == "" { 55 | return errors.New("receiver type illegal") 56 | } 57 | } 58 | 59 | _, err = client.Insert(receiver) 60 | if err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /app/context.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/http" 5 | 6 | netcontext "golang.org/x/net/context" 7 | 8 | "GoMailer/log" 9 | ) 10 | 11 | var ( 12 | contextKey = "holds a internal *context" 13 | ) 14 | 15 | type context struct { 16 | req *http.Request 17 | 18 | outCode int 19 | outHeader http.Header 20 | outBody []byte 21 | } 22 | 23 | func fromContext(ctx netcontext.Context) *context { 24 | c, _ := ctx.Value(&contextKey).(*context) 25 | return c 26 | } 27 | 28 | func withContext(parent netcontext.Context, c *context) netcontext.Context { 29 | ctx := netcontext.WithValue(parent, &contextKey, c) 30 | return ctx 31 | } 32 | 33 | func (c *context) Header() http.Header { return c.outHeader } 34 | 35 | func (c *context) Write(b []byte) (int, error) { 36 | if c.outCode == 0 { 37 | c.WriteHeader(http.StatusOK) 38 | } 39 | if len(b) > 0 && !bodyAllowedForStatus(c.outCode) { 40 | return 0, http.ErrBodyNotAllowed 41 | } 42 | c.outBody = append(c.outBody, b...) 43 | return len(b), nil 44 | } 45 | 46 | func (c *context) WriteHeader(code int) { 47 | if c.outCode != 0 { 48 | log.Error("WriteHeader called multiple times on request.") 49 | return 50 | } 51 | c.outCode = code 52 | } 53 | 54 | // Copied from $GOROOT/src/pkg/net/http/transfer.go. Some response status 55 | // codes do not permit a response body (nor response entity headers such as 56 | // Content-Length, Content-Type, etc). 57 | func bodyAllowedForStatus(status int) bool { 58 | switch { 59 | case status >= 100 && status <= 199: 60 | return false 61 | case status == 204: 62 | return false 63 | case status == 304: 64 | return false 65 | } 66 | return true 67 | } 68 | -------------------------------------------------------------------------------- /handler/mail/list.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | 8 | "GoMailer/app" 9 | "GoMailer/common/db" 10 | "GoMailer/common/key" 11 | "GoMailer/handler" 12 | "GoMailer/handler/endpoint" 13 | ) 14 | 15 | const ( 16 | defaultPageNum = 1 17 | defaultPageSize = 10 18 | ) 19 | 20 | type pageResult struct { 21 | PageNum int 22 | PageSize int 23 | Total int64 24 | List []*userMail 25 | } 26 | 27 | type userMail struct { 28 | InsertTime time.Time 29 | State string 30 | DeliveryTime db.Time 31 | Content string 32 | Raw map[string]string 33 | } 34 | 35 | func init() { 36 | router := handler.MailRouter.Path("/list").Subrouter() 37 | router.Methods(http.MethodGet).Handler(app.Handler(list)) 38 | } 39 | 40 | func list(w http.ResponseWriter, r *http.Request) (interface{}, *app.Error) { 41 | epk := key.EPKeyFromRequest(r) 42 | ep, err := endpoint.FindByKey(epk) 43 | if err != nil { 44 | return nil, app.Errorf(err, "failed to find endpoint by key") 45 | } 46 | userId := ep.UserId 47 | pageNum, pageSize := parsePageCondition(r) 48 | 49 | res, total, err := Find(userId, pageNum, pageSize) 50 | if err != nil { 51 | return nil, app.Errorf(err, "failed to find user post") 52 | } 53 | 54 | return &pageResult{ 55 | PageNum: pageNum, 56 | PageSize: pageSize, 57 | Total: total, 58 | List: res, 59 | }, nil 60 | } 61 | 62 | func parsePageCondition(r *http.Request) (int, int) { 63 | pn := r.URL.Query().Get("pn") 64 | ps := r.URL.Query().Get("ps") 65 | 66 | pni, err := strconv.Atoi(pn) 67 | if err != nil { 68 | pni = defaultPageNum 69 | } 70 | psi, err := strconv.Atoi(ps) 71 | if err != nil { 72 | psi = defaultPageSize 73 | } 74 | return pni, psi 75 | } 76 | -------------------------------------------------------------------------------- /handler/endpoint/preference/common.go: -------------------------------------------------------------------------------- 1 | package preference 2 | 3 | import ( 4 | "errors" 5 | 6 | "GoMailer/common/db" 7 | "GoMailer/common/utils" 8 | ) 9 | 10 | func FindByEndpoint(endpointId int64) (*db.EndpointPreference, error) { 11 | client, err := db.NewClient() 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | u := &db.EndpointPreference{} 17 | has, err := client.Where("endpoint_id = ?", endpointId).Get(u) 18 | if err != nil { 19 | return nil, err 20 | } 21 | if !has { 22 | return nil, nil 23 | } 24 | 25 | return u, nil 26 | } 27 | 28 | func Create(p *db.EndpointPreference) (*db.EndpointPreference, error) { 29 | if !utils.IsBlankStr(p.DeliverStrategy) && db.DeliverStrategy(p.DeliverStrategy) == "" { 30 | return nil, errors.New("deliver strategy illegal") 31 | } 32 | 33 | client, err := db.NewClient() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | endpointExist, err := client.ID(p.EndpointId).Exist(&db.Endpoint{}) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if !endpointExist { 43 | return nil, errors.New("endpoint not exist") 44 | } 45 | 46 | affected, err := client.InsertOne(p) 47 | if err != nil { 48 | return nil, err 49 | } 50 | if affected != 1 { 51 | return nil, errors.New("failed to InsertOne endpoint") 52 | } 53 | 54 | return p, nil 55 | } 56 | 57 | func Update(p *db.EndpointPreference) (*db.EndpointPreference, error) { 58 | if !utils.IsBlankStr(p.DeliverStrategy) && db.DeliverStrategy(p.DeliverStrategy) == "" { 59 | return nil, errors.New("deliver strategy illegal") 60 | } 61 | 62 | client, err := db.NewClient() 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | _, err = client.Where("endpoint_id = ?", p.EndpointId).Update(p) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return p, nil 72 | } 73 | -------------------------------------------------------------------------------- /middleware/cors/cors.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | 8 | "GoMailer/common/key" 9 | "GoMailer/handler/endpoint" 10 | "GoMailer/handler/userapp" 11 | "GoMailer/log" 12 | ) 13 | 14 | var ( 15 | freeAPI = map[string]struct{}{ 16 | "/api/shortcut": {}, 17 | } 18 | ) 19 | 20 | func CORS(r *mux.Router) func(http.Handler) http.Handler { 21 | // required so we don't get a code 405 22 | r.Methods(http.MethodOptions).Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) 23 | 24 | return func(next http.Handler) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | allowOrigin := "*" 27 | if _, ok := freeAPI[r.URL.Path]; !ok { 28 | // No need to verify, already pass the Guard. 29 | ak := key.EPKeyFromRequest(r) 30 | ep, err := endpoint.FindByKey(ak) 31 | if err != nil { 32 | log.Error("got error when find host for CORS origin: end point not exist") 33 | http.Error(w, err.Error(), http.StatusInternalServerError) 34 | return 35 | } 36 | app, err := userapp.FindById(ep.UserAppId) 37 | if err != nil { 38 | log.Error("got error when find host for CORS origin: user app not exist") 39 | http.Error(w, err.Error(), http.StatusInternalServerError) 40 | return 41 | } 42 | allowOrigin = app.Host 43 | } 44 | 45 | w.Header().Set("Access-Control-Allow-Origin", allowOrigin) 46 | w.Header().Set("Access-Control-Allow-Methods", "POST,GET,OPTIONS") 47 | w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept-Encoding, User-Agent, Accept") 48 | w.Header().Set("Access-Control-Allow-Credentials", "true") 49 | w.Header().Set("Access-Control-Max-Age", "86400") 50 | 51 | if r.Method == http.MethodOptions { 52 | // we only need headers for OPTIONS request, no need to go down the handler chain 53 | return 54 | } 55 | 56 | next.ServeHTTP(w, r) 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /handler/dialer/common.go: -------------------------------------------------------------------------------- 1 | package dialer 2 | 3 | import ( 4 | "errors" 5 | 6 | "GoMailer/common/db" 7 | "GoMailer/common/utils" 8 | ) 9 | 10 | func FindByName(userId int64, name string) (*db.Dialer, error) { 11 | if utils.IsBlankStr(name) { 12 | return nil, nil 13 | } 14 | 15 | client, err := db.NewClient() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | u := &db.Dialer{} 21 | has, err := client.Where("name = ? AND user_id = ?", name, userId).Get(u) 22 | if err != nil { 23 | return nil, err 24 | } 25 | if !has { 26 | return nil, nil 27 | } 28 | 29 | return u, nil 30 | } 31 | 32 | func Create(d *db.Dialer) (*db.Dialer, error) { 33 | if utils.IsBlankStr(d.Name) { 34 | return nil, errors.New("name can not be empty") 35 | } 36 | if utils.IsBlankStr(d.Host) { 37 | return nil, errors.New("host can not be empty") 38 | } 39 | if utils.IsBlankStr(d.AuthPassword) { 40 | return nil, errors.New("auth password can not be empty") 41 | } 42 | if utils.IsBlankStr(d.AuthUsername) { 43 | return nil, errors.New("auth username can not be empty") 44 | } 45 | if d.Port <= 0 { 46 | return nil, errors.New("port invalid") 47 | } 48 | 49 | client, err := db.NewClient() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | userExist, err := client.ID(d.UserId).Exist(&db.User{}) 55 | if err != nil { 56 | return nil, err 57 | } 58 | if !userExist { 59 | return nil, errors.New("user not exist") 60 | } 61 | 62 | affected, err := client.InsertOne(d) 63 | if err != nil { 64 | return nil, err 65 | } 66 | if affected != 1 { 67 | return nil, errors.New("failed to InsertOne dialer") 68 | } 69 | 70 | return d, nil 71 | } 72 | 73 | func Update(d *db.Dialer) (*db.Dialer, error) { 74 | if d.Port < 0 { 75 | return nil, errors.New("port invalid") 76 | } 77 | 78 | client, err := db.NewClient() 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | if d.UserId != 0 { 84 | userExist, err := client.ID(d.UserId).Exist(&db.User{}) 85 | if err != nil { 86 | return nil, err 87 | } 88 | if !userExist { 89 | return nil, errors.New("user not exist") 90 | } 91 | } 92 | 93 | _, err = client.ID(d.Id).Update(d) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return d, nil 98 | } 99 | -------------------------------------------------------------------------------- /common/key/key.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "math/rand" 8 | "net/http" 9 | "time" 10 | 11 | "GoMailer/common/utils" 12 | "GoMailer/conf" 13 | ) 14 | 15 | const ( 16 | EPKeyName = "EPKey" 17 | ReCaptchaTokenKeyName = "grecaptcha_token" 18 | ) 19 | 20 | func ReCaptchaKeyFromRequest(r *http.Request) string { 21 | token := r.URL.Query().Get(ReCaptchaTokenKeyName) 22 | if !utils.IsBlankStr(token) { 23 | return token 24 | } 25 | 26 | token = r.Header.Get(ReCaptchaTokenKeyName) 27 | if !utils.IsBlankStr(token) { 28 | return token 29 | } 30 | 31 | return getFromForm(r, ReCaptchaTokenKeyName) 32 | } 33 | 34 | func getFromForm(r *http.Request, keyName string) string { 35 | for k, vs := range r.Form { 36 | if k == keyName { 37 | return vs[0] 38 | } 39 | } 40 | 41 | return "" 42 | } 43 | 44 | func EPKeyFromRequest(r *http.Request) string { 45 | appKey := r.URL.Query().Get(EPKeyName) 46 | if !utils.IsBlankStr(appKey) { 47 | return appKey 48 | } 49 | 50 | appKey = r.Header.Get(EPKeyName) 51 | if !utils.IsBlankStr(appKey) { 52 | return appKey 53 | } 54 | 55 | return getFromForm(r, EPKeyName) 56 | } 57 | 58 | func GenerateKey() string { 59 | const str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 60 | const keyLen = 10 61 | bytes := []byte(str) 62 | var result []byte 63 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 64 | for i := 0; i < keyLen; i++ { 65 | result = append(result, bytes[r.Intn(len(bytes))]) 66 | } 67 | 68 | return string(result) 69 | } 70 | 71 | func VerifyReCaptcha(token string) (bool, error) { 72 | if utils.IsBlankStr(token) { 73 | return false, errors.New("reCaptcha token is empty") 74 | } 75 | 76 | const addr = "https://recaptcha.net/recaptcha/api/siteverify?secret=%s&response=%s" 77 | resp, err := http.Get(fmt.Sprintf(addr, conf.ReCaptchaSecret(), token)) 78 | if err != nil { 79 | return false, err 80 | } 81 | m := struct { 82 | ChallengeTs time.Time `json:"challenge_ts"` 83 | Score float32 `json:"score"` 84 | Hostname string `json:"hostname"` 85 | Success bool `json:"success"` 86 | }{} 87 | err = json.NewDecoder(resp.Body).Decode(&m) 88 | if err != nil { 89 | return false, err 90 | } 91 | 92 | return m.Success, nil 93 | } 94 | -------------------------------------------------------------------------------- /handler/userapp/common.go: -------------------------------------------------------------------------------- 1 | package userapp 2 | 3 | import ( 4 | "errors" 5 | 6 | "GoMailer/common/db" 7 | "GoMailer/common/utils" 8 | ) 9 | 10 | func FindById(appId int64) (*db.UserApp, error) { 11 | client, err := db.NewClient() 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | app := &db.UserApp{} 17 | has, err := client.Id(appId).Get(app) 18 | if err != nil { 19 | return nil, err 20 | } 21 | if !has { 22 | return nil, errors.New("app not exist") 23 | } 24 | 25 | return app, nil 26 | } 27 | 28 | func FindByName(userId int64, name string) (*db.UserApp, error) { 29 | if utils.IsBlankStr(name) { 30 | return nil, nil 31 | } 32 | 33 | client, err := db.NewClient() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | u := &db.UserApp{} 39 | has, err := client.Where("name = ? AND user_id = ?", name, userId).Get(u) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if !has { 44 | return nil, nil 45 | } 46 | 47 | return u, nil 48 | } 49 | 50 | func Create(ua *db.UserApp) (*db.UserApp, error) { 51 | if utils.IsBlankStr(ua.Name) { 52 | return nil, errors.New("name can not be empty") 53 | } 54 | if utils.IsBlankStr(ua.Host) { 55 | return nil, errors.New("host can not be empty") 56 | } 57 | 58 | if utils.IsBlankStr(ua.AppType) { 59 | ua.AppType = db.AppType_WEB.Name() // Default is WEB app. 60 | } else { 61 | if db.AppType(ua.AppType) == "" { 62 | return nil, errors.New("app type illegal") 63 | } 64 | } 65 | 66 | client, err := db.NewClient() 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | userExist, err := client.ID(ua.UserId).Exist(&db.User{}) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if !userExist { 76 | return nil, errors.New("user not exist") 77 | } 78 | 79 | affected, err := client.InsertOne(ua) 80 | if err != nil { 81 | return nil, err 82 | } 83 | if affected != 1 { 84 | return nil, errors.New("failed to InsertOne user app") 85 | } 86 | 87 | return ua, nil 88 | } 89 | 90 | func Update(ua *db.UserApp) (*db.UserApp, error) { 91 | client, err := db.NewClient() 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | if ua.UserId != 0 { 97 | userExist, err := client.ID(ua.UserId).Exist(&db.User{}) 98 | if err != nil { 99 | return nil, err 100 | } 101 | if !userExist { 102 | return nil, errors.New("user not exist") 103 | } 104 | } 105 | 106 | _, err = client.ID(ua.Id).Update(ua) 107 | if err != nil { 108 | return nil, err 109 | } 110 | return ua, nil 111 | } 112 | -------------------------------------------------------------------------------- /app/api.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "runtime" 9 | 10 | "GoMailer/log" 11 | ) 12 | 13 | const ( 14 | HeaderContentType = "Content-Type" 15 | 16 | MimeApplicationJSON = "application/json" 17 | ) 18 | 19 | type Handler func(http.ResponseWriter, *http.Request) (interface{}, *Error) 20 | 21 | type Error struct { 22 | Error error 23 | Message string 24 | Code int 25 | } 26 | 27 | func (fn Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 28 | m, e := fn(w, r) 29 | 30 | writeError := func(err *Error) { 31 | msg := fmt.Sprintf("handler error, status code: %d, message: %s, underlying err: %v", 32 | err.Code, err.Message, err.Error) 33 | log.Error(msg) 34 | http.Error(w, msg, err.Code) 35 | } 36 | if e != nil { 37 | writeError(e) 38 | return 39 | } 40 | 41 | w.Header().Set(HeaderContentType, MimeApplicationJSON) 42 | if err := json.NewEncoder(w).Encode(m); err != nil { 43 | e := Errorf(err, "failed to write to http response") 44 | writeError(e) 45 | } 46 | } 47 | 48 | func Errorf(err error, format string, v ...interface{}) *Error { 49 | return &Error{ 50 | Error: err, 51 | Message: fmt.Sprintf(format, v...), 52 | Code: 500, 53 | } 54 | } 55 | 56 | func RootHandler() http.HandlerFunc { 57 | return handleHTTP 58 | } 59 | 60 | func handleHTTP(w http.ResponseWriter, r *http.Request) { 61 | c := &context{ 62 | req: r, 63 | outHeader: w.Header(), 64 | } 65 | r = r.WithContext(withContext(r.Context(), c)) 66 | c.req = r 67 | 68 | executeRequestSafely(c, r) 69 | if c.outCode != 0 { 70 | w.WriteHeader(c.outCode) 71 | } 72 | if c.outBody != nil { 73 | w.Write(c.outBody) 74 | } 75 | } 76 | 77 | func executeRequestSafely(c *context, r *http.Request) { 78 | defer func() { 79 | if x := recover(); x != nil { 80 | log.Errorf("%s", renderPanic(x)) 81 | c.outCode = 500 82 | } 83 | }() 84 | 85 | log.Info("receive request: " + r.URL.String()) 86 | http.DefaultServeMux.ServeHTTP(c, r) 87 | } 88 | 89 | func renderPanic(x interface{}) string { 90 | buf := make([]byte, 16<<10) // 16 KB should be plenty 91 | buf = buf[:runtime.Stack(buf, false)] 92 | 93 | const ( 94 | skipStart = "app.renderPanic" 95 | skipFrames = 2 96 | ) 97 | start := bytes.Index(buf, []byte(skipStart)) 98 | p := start 99 | for i := 0; i < skipFrames*2 && p+1 < len(buf); i++ { 100 | p = bytes.IndexByte(buf[p+1:], '\n') + p + 1 101 | if p < 0 { 102 | break 103 | } 104 | } 105 | if p >= 0 { 106 | copy(buf[start:], buf[p+1:]) 107 | buf = buf[:len(buf)-(p+1-start)] 108 | } 109 | 110 | // Add panic heading. 111 | head := fmt.Sprintf("panic: %v\n\n", x) 112 | if len(head) > len(buf) { 113 | // Extremely unlikely to happen. 114 | return head 115 | } 116 | copy(buf[len(head):], buf) 117 | copy(buf, head) 118 | 119 | return string(buf) 120 | } 121 | -------------------------------------------------------------------------------- /handler/endpoint/common.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "errors" 5 | 6 | "GoMailer/common/db" 7 | "GoMailer/common/utils" 8 | ) 9 | 10 | func FindByName(appId int64, name string) (*db.Endpoint, error) { 11 | if utils.IsBlankStr(name) { 12 | return nil, nil 13 | } 14 | 15 | client, err := db.NewClient() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | u := &db.Endpoint{} 21 | has, err := client.Where("name = ? AND user_app_id = ?", name, appId).Get(u) 22 | if err != nil { 23 | return nil, err 24 | } 25 | if !has { 26 | return nil, nil 27 | } 28 | 29 | return u, nil 30 | } 31 | 32 | func Create(ep *db.Endpoint) (*db.Endpoint, error) { 33 | if utils.IsBlankStr(ep.Name) { 34 | return nil, errors.New("name can not be empty") 35 | } 36 | 37 | client, err := db.NewClient() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | userExist, err := client.ID(ep.UserId).Exist(&db.User{}) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if !userExist { 47 | return nil, errors.New("user not exist") 48 | } 49 | userAppExist, err := client.ID(ep.UserAppId).Exist(&db.UserApp{}) 50 | if err != nil { 51 | return nil, err 52 | } 53 | if !userAppExist { 54 | return nil, errors.New("user app not exist") 55 | } 56 | dialerExist, err := client.ID(ep.DialerId).Exist(&db.Dialer{}) 57 | if err != nil { 58 | return nil, err 59 | } 60 | if !dialerExist { 61 | return nil, errors.New("dialer not exist") 62 | } 63 | // Nullable. 64 | if ep.TemplateId != 0 { 65 | templateExist, err := client.ID(ep.TemplateId).Exist(&db.Template{}) 66 | if err != nil { 67 | return nil, err 68 | } 69 | if !templateExist { 70 | return nil, errors.New("template not exist") 71 | } 72 | } 73 | 74 | affected, err := client.InsertOne(ep) 75 | if err != nil { 76 | return nil, err 77 | } 78 | if affected != 1 { 79 | return nil, errors.New("failed to InsertOne endpoint") 80 | } 81 | 82 | return ep, nil 83 | } 84 | 85 | func Update(ep *db.Endpoint) (*db.Endpoint, error) { 86 | client, err := db.NewClient() 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | if ep.UserId != 0 { 92 | userExist, err := client.ID(ep.UserId).Exist(&db.User{}) 93 | if err != nil { 94 | return nil, err 95 | } 96 | if !userExist { 97 | return nil, errors.New("user not exist") 98 | } 99 | } 100 | if ep.UserAppId != 0 { 101 | userAppExist, err := client.ID(ep.UserAppId).Exist(&db.UserApp{}) 102 | if err != nil { 103 | return nil, err 104 | } 105 | if !userAppExist { 106 | return nil, errors.New("user app not exist") 107 | } 108 | } 109 | if ep.DialerId != 0 { 110 | dialerExist, err := client.ID(ep.DialerId).Exist(&db.Dialer{}) 111 | if err != nil { 112 | return nil, err 113 | } 114 | if !dialerExist { 115 | return nil, errors.New("dialer not exist") 116 | } 117 | } 118 | if ep.TemplateId != 0 { 119 | templateExist, err := client.ID(ep.TemplateId).Exist(&db.Template{}) 120 | if err != nil { 121 | return nil, err 122 | } 123 | if !templateExist { 124 | return nil, errors.New("template not exist") 125 | } 126 | } 127 | 128 | _, err = client.ID(ep.Id).Update(ep) 129 | if err != nil { 130 | return nil, err 131 | } 132 | return ep, nil 133 | } 134 | -------------------------------------------------------------------------------- /handler/mail/common.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "GoMailer/common/db" 10 | "GoMailer/common/utils" 11 | ) 12 | 13 | func Find(userId int64, pageNum int, pageSize int) ([]*userMail, int64, error) { 14 | mt, err := handleUserMailTable(userId) 15 | if err != nil { 16 | return nil, 0, err 17 | } 18 | 19 | client, err := db.NewClient() 20 | if err != nil { 21 | return nil, 0, err 22 | } 23 | total, err := client.Table(mt).Count(&db.Mail{}) 24 | if err != nil { 25 | return nil, 0, err 26 | } 27 | 28 | var ms []*db.Mail 29 | err = client.Table(mt).Limit(pageSize, (pageNum-1)*pageSize).Desc("insert_time").Find(&ms) 30 | if err != nil { 31 | return nil, 0, err 32 | } 33 | ums := make([]*userMail, 0, len(ms)) 34 | for _, m := range ms { 35 | raw := make(map[string]string) 36 | err := json.Unmarshal([]byte(m.Raw), &raw) 37 | if err != nil { 38 | return nil, 0, err 39 | } 40 | ums = append(ums, &userMail{ 41 | InsertTime: m.InsertTime, 42 | State: m.State, 43 | DeliveryTime: m.DeliveryTime, 44 | Content: m.Content, 45 | Raw: raw, 46 | }) 47 | } 48 | 49 | return ums, total, nil 50 | } 51 | 52 | func Create(userId int64, mail *db.Mail) (*db.Mail, error) { 53 | if utils.IsBlankStr(mail.Content) { 54 | return nil, errors.New("mail content can not be empty") 55 | } 56 | 57 | mt, err := handleUserMailTable(userId) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | client, err := db.NewClient() 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | affected, err := client.Table(mt).InsertOne(mail) 68 | if err != nil { 69 | return nil, err 70 | } 71 | if affected != 1 { 72 | return nil, errors.New("failed to InsertOne mail") 73 | } 74 | 75 | return mail, nil 76 | } 77 | 78 | func handleUserMailTable(userId int64) (string, error) { 79 | client, err := db.NewClient() 80 | if err != nil { 81 | return "", err 82 | } 83 | 84 | mt := getUserMailTableName(userId) 85 | res, err := client.Query("SHOW TABLES") 86 | if err != nil { 87 | return "", err 88 | } 89 | mtExist := false 90 | for _, r := range res { 91 | if string(r["Tables_in_gomailer"]) == mt { 92 | mtExist = true 93 | } 94 | } 95 | if !mtExist { 96 | _, err = client.Exec(buildSql(mt)) 97 | if err != nil { 98 | return "", err 99 | } 100 | } 101 | 102 | return mt, nil 103 | } 104 | 105 | func buildSql(tableName string) string { 106 | sql := strings.Builder{} 107 | sql.WriteString(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s` (", tableName)) 108 | sql.WriteString(" `id` int unsigned NOT NULL AUTO_INCREMENT,") 109 | sql.WriteString(" `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,") 110 | sql.WriteString(" `endpoint_id` int NOT NULL,") 111 | sql.WriteString(" `state` varchar(100) NOT NULL,") 112 | sql.WriteString(" `delivery_time` timestamp NULL DEFAULT NULL,") 113 | sql.WriteString(" `content` longtext NOT NULL,") 114 | sql.WriteString(" `raw` longtext NOT NULL,") 115 | sql.WriteString(" PRIMARY KEY (`id`)") 116 | sql.WriteString(") ENGINE=InnoDB DEFAULT CHARSET=utf8;") 117 | return sql.String() 118 | } 119 | 120 | func getUserMailTableName(userId int64) string { 121 | return fmt.Sprintf("mail_%d", userId) 122 | } 123 | -------------------------------------------------------------------------------- /handler/mail/send.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | 8 | "GoMailer/app" 9 | "GoMailer/common/db" 10 | "GoMailer/common/key" 11 | "GoMailer/common/utils" 12 | "GoMailer/handler" 13 | "GoMailer/handler/endpoint" 14 | "GoMailer/handler/userapp" 15 | "GoMailer/log" 16 | ) 17 | 18 | func init() { 19 | router := handler.MailRouter.Path("/send").Subrouter() 20 | router.Methods(http.MethodPost).Handler(app.Handler(send)) 21 | } 22 | 23 | func send(w http.ResponseWriter, r *http.Request) (interface{}, *app.Error) { 24 | if err := r.ParseForm(); err != nil { 25 | return nil, app.Errorf(err, "failed to parse form") 26 | } 27 | epk := key.EPKeyFromRequest(r) 28 | ep, err := endpoint.FindByKey(epk) 29 | if err != nil { 30 | return nil, app.Errorf(err, "failed to find endpoint by key") 31 | } 32 | 33 | raw, err := parseForm(r) 34 | if err != nil { 35 | setupRedirectHeader(w, ep, err, r) 36 | return nil, app.Errorf(err, "got error when parse form") 37 | } 38 | mail, err := handleMail(ep.Id, raw, key.ReCaptchaKeyFromRequest(r)) 39 | if mail != nil { 40 | _, err := Create(ep.UserId, mail) 41 | if err != nil { 42 | setupRedirectHeader(w, ep, err, r) 43 | return nil, app.Errorf(err, "fail to store mail") 44 | } 45 | } 46 | if err != nil { 47 | setupRedirectHeader(w, ep, err, r) 48 | return nil, app.Errorf(err, "failed to deliver mail") 49 | } 50 | 51 | setupRedirectHeader(w, ep, nil, r) 52 | return nil, nil 53 | } 54 | 55 | func setupRedirectHeader(w http.ResponseWriter, ep *db.Endpoint, oerr error, r *http.Request) { 56 | client, err := db.NewClient() 57 | if err != nil { 58 | http.Error(w, err.Error(), http.StatusInternalServerError) 59 | log.Errorf("got err when set up redirect header: %v", err) 60 | return 61 | } 62 | 63 | epp := &db.EndpointPreference{} 64 | get, err := client.Where("endpoint_id = ?", ep.Id).Get(epp) 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusInternalServerError) 67 | log.Errorf("got err when set up redirect header: %v", err) 68 | return 69 | } 70 | if !get { 71 | // No preference yet, ignore. 72 | return 73 | } 74 | ua, err := userapp.FindById(ep.UserAppId) 75 | if err != nil { 76 | http.Error(w, err.Error(), http.StatusInternalServerError) 77 | log.Errorf("got err when set up redirect header: %v", err) 78 | return 79 | } 80 | 81 | setup := func(addr string) { 82 | if ua.AppType == db.AppType_AMP_WEB.Name() { 83 | w.Header().Set("AMP-Redirect-To", addr) 84 | w.Header().Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin, AMP-Redirect-To") 85 | return 86 | } 87 | 88 | w.WriteHeader(http.StatusFound) 89 | w.Header().Set("Location", addr) 90 | } 91 | if oerr != nil && !utils.IsBlankStr(epp.FailRedirect) { 92 | setup(epp.FailRedirect + "?err=" + oerr.Error()) 93 | } 94 | if oerr == nil && !utils.IsBlankStr(epp.SuccessRedirect) { 95 | setup(epp.SuccessRedirect) 96 | } 97 | } 98 | 99 | func parseForm(r *http.Request) (map[string]string, error) { 100 | data := make(map[string]string) 101 | allBlank := true 102 | for k, vs := range r.Form { 103 | if k == key.EPKeyName || k == key.ReCaptchaTokenKeyName { 104 | continue 105 | } 106 | 107 | str := strings.TrimSpace(vs[0]) 108 | if len(str) > 0 { 109 | allBlank = false 110 | } 111 | data[k] = str 112 | } 113 | if allBlank { 114 | return nil, errors.New("not allow to send empty content") 115 | } 116 | 117 | return data, nil 118 | } 119 | -------------------------------------------------------------------------------- /common/db/entity.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type deliverStrategy string 8 | type receiverType string 9 | type mailState string 10 | type appType string 11 | 12 | const ( 13 | DeliverStrategy_DELIVER_IMMEDIATELY = deliverStrategy("DELIVER_IMMEDIATELY") 14 | DeliverStrategy_STAGING = deliverStrategy("STAGING") 15 | 16 | MailState_STAGING = mailState("STAGING") 17 | MailState_DELIVER_SUCCESS = mailState("DELIVER_SUCCESS") 18 | MailState_DELIVER_FAILED = mailState("DELIVER_FAILED") 19 | 20 | ReceiverType_To = receiverType("To") 21 | ReceiverType_Cc = receiverType("Cc") 22 | ReceiverType_Bcc = receiverType("Bcc") 23 | 24 | AppType_WEB = appType("WEB") 25 | AppType_AMP_WEB = appType("AMP_WEB") 26 | ) 27 | 28 | var ( 29 | deliverStrategyName = []string{ 30 | "DELIVER_IMMEDIATELY", 31 | "STAGING", 32 | } 33 | 34 | receiverTypeName = []string{ 35 | "To", 36 | "Cc", 37 | "Bcc", 38 | } 39 | 40 | mailStateName = []string{ 41 | "STAGING", 42 | "DELIVER_SUCCESS", 43 | "DELIVER_FAILED", 44 | } 45 | 46 | appTypeName = []string{ 47 | "WEB", 48 | "AMP_WEB", 49 | } 50 | ) 51 | 52 | type User struct { 53 | Id int64 54 | InsertTime time.Time `xorm:"created"` 55 | 56 | Username string 57 | Password string 58 | } 59 | 60 | type UserApp struct { 61 | Id int64 62 | InsertTime time.Time `xorm:"created"` 63 | 64 | UserId int64 65 | AppType string 66 | 67 | Name string 68 | Host string 69 | } 70 | 71 | type Endpoint struct { 72 | Id int64 73 | InsertTime time.Time `xorm:"created"` 74 | 75 | UserAppId int64 76 | DialerId int64 77 | TemplateId int64 78 | UserId int64 79 | 80 | Name string 81 | Key string 82 | } 83 | 84 | type EndpointPreference struct { 85 | Id int64 86 | InsertTime time.Time `xorm:"created"` 87 | 88 | EndpointId int64 89 | 90 | DeliverStrategy string 91 | EnableReCaptcha int32 // 1 enable 2 disable 92 | SuccessRedirect string 93 | FailRedirect string 94 | } 95 | 96 | type Receiver struct { 97 | Id int64 98 | InsertTime time.Time `xorm:"created"` 99 | 100 | EndpointId int64 101 | UserId int64 102 | UserAppId int64 103 | 104 | Address string 105 | ReceiverType string 106 | } 107 | 108 | type Dialer struct { 109 | Id int64 110 | InsertTime time.Time `xorm:"created"` 111 | 112 | UserId int64 113 | 114 | Host string 115 | Port int 116 | AuthUsername string 117 | AuthPassword string 118 | 119 | Name string 120 | } 121 | 122 | type Template struct { 123 | Id int64 124 | InsertTime time.Time `xorm:"created"` 125 | 126 | UserId int64 127 | 128 | Template string 129 | ContentType string 130 | } 131 | 132 | type Mail struct { 133 | Id int64 134 | InsertTime time.Time `xorm:"created"` 135 | 136 | EndpointId int64 137 | State string 138 | DeliveryTime Time 139 | Content string 140 | Raw string 141 | } 142 | 143 | type Time time.Time 144 | 145 | func (t Time) MarshalJSON() ([]byte, error) { 146 | tm := time.Time(t) 147 | if tm.IsZero() { 148 | return []byte(`""`), nil 149 | } 150 | return tm.MarshalJSON() 151 | } 152 | 153 | func ReceiverType(name string) receiverType { 154 | for _, n := range receiverTypeName { 155 | if name == n { 156 | return receiverType(name) 157 | } 158 | } 159 | 160 | return "" 161 | } 162 | 163 | func DeliverStrategy(name string) deliverStrategy { 164 | for _, n := range deliverStrategyName { 165 | if name == n { 166 | return deliverStrategy(name) 167 | } 168 | } 169 | 170 | return "" 171 | } 172 | 173 | func MailState(name string) mailState { 174 | for _, n := range mailStateName { 175 | if name == n { 176 | return mailState(name) 177 | } 178 | } 179 | 180 | return "" 181 | } 182 | 183 | func AppType(name string) appType { 184 | for _, n := range appTypeName { 185 | if name == n { 186 | return appType(name) 187 | } 188 | } 189 | 190 | return "" 191 | } 192 | 193 | func (r receiverType) Name() string { 194 | return string(r) 195 | } 196 | 197 | func (r deliverStrategy) Name() string { 198 | return string(r) 199 | } 200 | 201 | func (r mailState) Name() string { 202 | return string(r) 203 | } 204 | 205 | func (r appType) Name() string { 206 | return string(r) 207 | } 208 | -------------------------------------------------------------------------------- /handler/mail/sender.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "gopkg.in/gomail.v2" 11 | 12 | "GoMailer/common/db" 13 | "GoMailer/common/key" 14 | "GoMailer/log" 15 | ) 16 | 17 | func handleMail(endpointId int64, raw map[string]string, reCaptchaKey string) (*db.Mail, error) { 18 | client, err := db.NewClient() 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | edp := &db.EndpointPreference{} 24 | get, err := client.Where("endpoint_id = ?", endpointId).Get(edp) 25 | if err != nil { 26 | return nil, err 27 | } 28 | if !get { 29 | edp = &db.EndpointPreference{ 30 | DeliverStrategy: db.DeliverStrategy_DELIVER_IMMEDIATELY.Name(), 31 | EnableReCaptcha: 2, // Default is disable reCaptcha. 32 | } 33 | } 34 | 35 | if edp.EnableReCaptcha == 1 { 36 | ok, err := key.VerifyReCaptcha(reCaptchaKey) 37 | if err != nil { 38 | return nil, err 39 | } 40 | if !ok { 41 | return nil, errors.New("reCaptcha verify failed") 42 | } 43 | } 44 | 45 | message, content, err := prepareMessage(endpointId, raw) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | mail := &db.Mail{} 51 | mail.Content = content 52 | bytes, err := json.Marshal(raw) 53 | if err != nil { 54 | return nil, err 55 | } 56 | mail.Raw = string(bytes) 57 | mail.EndpointId = endpointId 58 | mail.State = db.MailState_STAGING.Name() 59 | if edp.DeliverStrategy == db.DeliverStrategy_DELIVER_IMMEDIATELY.Name() { 60 | dialer, err := getMailDialer(endpointId) 61 | if err != nil { 62 | return nil, err 63 | } 64 | log.Infof("deliver mail: %+v", message) 65 | 66 | err = dialer.DialAndSend(message) 67 | mail.DeliveryTime = db.Time(time.Now()) 68 | if err != nil { 69 | mail.State = db.MailState_DELIVER_FAILED.Name() 70 | return nil, err 71 | } 72 | mail.State = db.MailState_DELIVER_SUCCESS.Name() 73 | } 74 | 75 | return mail, nil 76 | } 77 | 78 | func prepareMessage(endpointId int64, val map[string]string) (*gomail.Message, string, error) { 79 | client, err := db.NewClient() 80 | if err != nil { 81 | return nil, "", err 82 | } 83 | 84 | ed := &db.Endpoint{} 85 | get, err := client.Id(endpointId).Get(ed) 86 | if err != nil { 87 | return nil, "", err 88 | } 89 | if !get { 90 | return nil, "", errors.New("endpoint not exist") 91 | } 92 | 93 | d := &db.Dialer{} 94 | _, err = client.Id(ed.DialerId).Get(d) 95 | if err != nil { 96 | return nil, "", err 97 | } 98 | 99 | // Prepare receiver. 100 | rs := make([]db.Receiver, 0) 101 | err = client.Where("endpoint_id = ?", ed.Id).Find(&rs) 102 | if err != nil { 103 | return nil, "", err 104 | } 105 | 106 | // Prepare template. 107 | t := &db.Template{} 108 | get, err = client.Id(ed.TemplateId).Get(t) 109 | if err != nil { 110 | return nil, "", err 111 | } 112 | template, contentType := getDefaultTemplate(val) 113 | if get { 114 | template, contentType = t.Template, t.ContentType 115 | } 116 | 117 | msg := gomail.NewMessage() 118 | msg.SetHeader("From", msg.FormatAddress(d.AuthUsername, d.Name)) 119 | rsMap := make(map[string][]string) 120 | for _, r := range rs { 121 | rsMap[r.ReceiverType] = append(rsMap[r.ReceiverType], r.Address) 122 | } 123 | for t, e := range rsMap { 124 | msg.SetHeader(t, e...) 125 | } 126 | msg.SetHeader("Subject", ed.Name) 127 | content := parseContent(template, val) 128 | msg.SetBody(contentType, content) 129 | 130 | return msg, content, nil 131 | } 132 | 133 | func getMailDialer(endpointId int64) (*gomail.Dialer, error) { 134 | client, err := db.NewClient() 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | ed := &db.Endpoint{} 140 | get, err := client.Id(endpointId).Get(ed) 141 | if err != nil { 142 | return nil, err 143 | } 144 | if !get { 145 | return nil, errors.New("endpoint not exist") 146 | } 147 | 148 | d := &db.Dialer{} 149 | _, err = client.Id(ed.DialerId).Get(d) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | return gomail.NewDialer(d.Host, d.Port, d.AuthUsername, d.AuthPassword), nil 155 | } 156 | 157 | func parseContent(t string, val map[string]string) string { 158 | for key, value := range val { 159 | t = strings.ReplaceAll(t, fmt.Sprintf("{{%s}}", key), fmt.Sprintf("%v", value)) 160 | } 161 | t = strings.ReplaceAll(t, "}}", "\"") 162 | t = strings.ReplaceAll(t, "{{", "\"") 163 | return t 164 | } 165 | 166 | func getDefaultTemplate(val map[string]string) (string, string) { 167 | builder := strings.Builder{} 168 | for key := range val { 169 | builder.WriteString(fmt.Sprintf("%s: {{%s}}\n", key, key)) 170 | } 171 | return builder.String(), "text/plain" 172 | } 173 | -------------------------------------------------------------------------------- /gomailer.sql: -------------------------------------------------------------------------------- 1 | -- -------------------------------------------------------- 2 | -- 主机: 127.0.0.1 3 | -- 服务器版本: 8.0.19 - MySQL Community Server - GPL 4 | -- 服务器操作系统: Win64 5 | -- HeidiSQL 版本: 10.3.0.5771 6 | -- -------------------------------------------------------- 7 | 8 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 9 | /*!40101 SET NAMES utf8 */; 10 | /*!50503 SET NAMES utf8mb4 */; 11 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 12 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 13 | 14 | 15 | -- 导出 gomailer 的数据库结构 16 | CREATE DATABASE IF NOT EXISTS `gomailer` /*!40100 DEFAULT CHARACTER SET utf8 */ /*!80016 DEFAULT ENCRYPTION='N' */; 17 | USE `gomailer`; 18 | 19 | -- 导出 表 gomailer.dialer 结构 20 | CREATE TABLE IF NOT EXISTS `dialer` ( 21 | `id` int unsigned NOT NULL AUTO_INCREMENT, 22 | `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | `host` varchar(1000) NOT NULL, 24 | `port` int NOT NULL, 25 | `user_id` int unsigned NOT NULL, 26 | `auth_username` varchar(1000) NOT NULL, 27 | `auth_password` varchar(1000) NOT NULL, 28 | `name` varchar(500) DEFAULT NULL, 29 | PRIMARY KEY (`id`), 30 | UNIQUE KEY `user_id` (`user_id`,`name`) 31 | ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; 32 | 33 | -- 数据导出被取消选择。 34 | 35 | -- 导出 表 gomailer.endpoint 结构 36 | CREATE TABLE IF NOT EXISTS `endpoint` ( 37 | `id` int unsigned NOT NULL AUTO_INCREMENT, 38 | `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 39 | `user_app_id` int unsigned NOT NULL, 40 | `dialer_id` int unsigned NOT NULL, 41 | `template_id` int unsigned DEFAULT NULL, 42 | `user_id` int unsigned NOT NULL, 43 | `name` varchar(500) NOT NULL, 44 | `key` varchar(1000) NOT NULL, 45 | PRIMARY KEY (`id`), 46 | UNIQUE KEY `user_app_id` (`user_app_id`,`name`) 47 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; 48 | 49 | -- 数据导出被取消选择。 50 | 51 | -- 导出 表 gomailer.endpoint_preference 结构 52 | CREATE TABLE IF NOT EXISTS `endpoint_preference` ( 53 | `id` int unsigned NOT NULL AUTO_INCREMENT, 54 | `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 55 | `endpoint_id` int unsigned NOT NULL, 56 | `deliver_strategy` varchar(500) DEFAULT NULL, 57 | `enable_re_captcha` tinyint DEFAULT NULL, 58 | `success_redirect` text, 59 | `fail_redirect` text, 60 | PRIMARY KEY (`id`), 61 | UNIQUE KEY `end_point_id` (`endpoint_id`) 62 | ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; 63 | 64 | -- 数据导出被取消选择。 65 | 66 | -- 导出 表 gomailer.mail 结构 67 | CREATE TABLE IF NOT EXISTS `mail` ( 68 | `id` int unsigned NOT NULL AUTO_INCREMENT, 69 | `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 70 | `endpoint_id` int NOT NULL, 71 | `state` varchar(100) NOT NULL, 72 | `delivery_time` timestamp NULL DEFAULT NULL, 73 | `content` longtext NOT NULL, 74 | `raw` longtext NOT NULL, 75 | PRIMARY KEY (`id`) 76 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 77 | 78 | -- 数据导出被取消选择。 79 | 80 | -- 导出 表 gomailer.receiver 结构 81 | CREATE TABLE IF NOT EXISTS `receiver` ( 82 | `id` int unsigned NOT NULL AUTO_INCREMENT, 83 | `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 84 | `endpoint_id` int unsigned NOT NULL, 85 | `user_id` int unsigned NOT NULL, 86 | `user_app_id` int unsigned NOT NULL, 87 | `address` varchar(1000) NOT NULL, 88 | `receiver_type` varchar(500) NOT NULL, 89 | PRIMARY KEY (`id`) 90 | ) ENGINE=InnoDB AUTO_INCREMENT=78 DEFAULT CHARSET=utf8; 91 | 92 | -- 数据导出被取消选择。 93 | 94 | -- 导出 表 gomailer.template 结构 95 | CREATE TABLE IF NOT EXISTS `template` ( 96 | `id` int unsigned NOT NULL AUTO_INCREMENT, 97 | `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 98 | `user_id` int unsigned NOT NULL, 99 | `template` longtext NOT NULL, 100 | `content_type` varchar(100) DEFAULT NULL, 101 | PRIMARY KEY (`id`) 102 | ) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8; 103 | 104 | -- 数据导出被取消选择。 105 | 106 | -- 导出 表 gomailer.user 结构 107 | CREATE TABLE IF NOT EXISTS `user` ( 108 | `id` int unsigned NOT NULL AUTO_INCREMENT, 109 | `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 110 | `username` varchar(500) NOT NULL, 111 | `password` varchar(1000) NOT NULL, 112 | PRIMARY KEY (`id`), 113 | UNIQUE KEY `username` (`username`) 114 | ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; 115 | 116 | -- 数据导出被取消选择。 117 | 118 | -- 导出 表 gomailer.user_app 结构 119 | CREATE TABLE IF NOT EXISTS `user_app` ( 120 | `id` int unsigned NOT NULL AUTO_INCREMENT, 121 | `insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 122 | `user_id` int unsigned NOT NULL, 123 | `app_type` varchar(500) DEFAULT NULL, 124 | `name` varchar(500) NOT NULL, 125 | `host` varchar(1000) DEFAULT NULL, 126 | PRIMARY KEY (`id`), 127 | UNIQUE KEY `user_id` (`user_id`,`name`) 128 | ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; 129 | 130 | -- 数据导出被取消选择。 131 | 132 | /*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; 133 | /*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */; 134 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GoMailer 2 | 轻量电子邮件推送服务(A lightly email sending service for Go) 3 | 4 | 通过form提交用户输入的数据,GoMailer会将这些数据填入预先定义好的邮件内容模板中,并帮你把内容投递到指定的邮箱。 5 | 也可以选择把邮件暂存在GoMailer中,另外选择时间手动触发投递。 6 | 7 | 额外的可选配置:
8 | 1 支持开启reCaptcha验证,避免恶意投递
9 | 2 配置请求成功或失败时的重定向地址,相应事件发生时用户将被重定向到指定页面 10 | 11 | 可参考 [duanjn.com/#Feedback](http://duanjn.com) 部分的集成效果 12 | 13 | ## Release Note 14 | - [v0.1.0](https://github.com/DuanJiaNing/GoMailer/releases/tag/v0.1.0) 15 | 16 | ## 相关日志 17 | - [GoMailer - 用 Go 开发的轻量电子邮件推送服务](https://www.jianshu.com/p/158a25a452ca) 18 | 19 | ## 使用说明 20 | 提供三个接口与GoMailer进行交互,EPKey获取接口(EPKey唯一标识一个服务接入点),邮件发送接口,邮件查询接口。 21 | 22 | #### 1. 获取 EPKey 23 | API: `POST /api/shortcut` 24 | 25 | 将如下json作为 request body, 发送POST请求到`/api/shortcut`接口获取EPKey。 26 | ```json 27 | { 28 | "user": { 29 | "username": "A", 30 | "password": "123456" 31 | }, 32 | "app": { 33 | "name": "sample", 34 | "host": "sample.com", 35 | "appType": "WEB" 36 | }, 37 | "endpoint": { 38 | "name": "sample用户反馈", 39 | "dialer": { 40 | "host": "smtp.qq.com", 41 | "port": 465, 42 | "authUsername": "666@qq.com", 43 | "authPassword": "xxx", 44 | "name": "sample用户反馈专用" 45 | }, 46 | "receiver": [ 47 | { 48 | "address": "xxx@163.com", 49 | "receiverType": "To" 50 | }, 51 | { 52 | "address": "xxx@gmail.com", 53 | "receiverType": "Cc" 54 | }, 55 | { 56 | "address": "xxx1@gmail.com", 57 | "receiverType": "Bcc" 58 | } 59 | ], 60 | "template": { 61 | "contentType": "text/html", 62 | "template": "
来自用户[{{name}}]的反馈, 用户联系方式: {{contact}}, 反馈内容如下:

{{content}}

" 63 | }, 64 | "preference": { 65 | "deliverStrategy": "DELIVER_IMMEDIATELY", 66 | "enableReCaptcha": 1, 67 | "successRedirect": "http://www.sample.com/feedback-success.html", 68 | "failRedirect": "http://www.sample.com/feedback-fail.html" 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | 字段说明: 75 | - dialer: 邮件发件人配置,需到自己的邮箱网页端自行获取,参考[QQ邮箱的获取方式](https://service.mail.qq.com/cgi-bin/help?subtype=1&id=28&no=1001256) 76 | - dialer.name: 发件人名称 77 | - app.appType: 目前支持标准web应用和amp web应用,amp的重定向使用自定义的header,需要不同的逻辑处理。标准web: WEB, AMP_WEB: amp web应用 78 | - receiver: 收件人配置,To: 接收人,Cc: 抄送人,Bcc: 密送人 79 | - template: 邮件内容模板配置,类似{{contact}}的部分最终会被form中相应字段值替换 80 | - preference.deliverStrategy: 邮件投递策略: DELIVER_IMMEDIATELY: 立即发送,STAGING: 保存但不发送 81 | - preference.enableReCaptcha: 是否启用reCaptcha验证,1: 启用,2: 不启用 82 | - preference.successRedirect: 邮件发送成功时的重定向地址 83 | 84 | 请求成功时的返回结果 85 | ```text 86 | { 87 | "EPKey": "xxx" 88 | } 89 | ``` 90 | EPKey唯一标识一个服务接入点,后续请求都需要将该参数拼接在url中(或form中)传递给服务器。借用上面的请求示例进行说明,如有用户A,是 91 | sample网站的管理员,sample网站有两个地方接入了GoMailer的邮件服务,一个是用户反馈功能,一个是质量投诉功能,那么用户反馈就为一个 92 | 接入点,质量投诉为另一个接入点,EPKey不同,拥有独立的配置。 93 | 94 | 可将上述request body保存为文件,后使用register.sh脚本快捷获取EPKey: 95 | ```shell script 96 | # http://localhost:8080/api/shortcut 为接口地址,注意进行替换 97 | # sample.json 为接入点配置文件 98 | ./register.sh http://localhost:6060/api/shortcut sample.json 99 | ``` 100 | 101 | 当接入点配置更新时亦可通过该接口进行更新。 102 | 103 | #### 2. 在网站中集成 104 | API: `POST /api/mail/send` 105 | 106 | 将第一步获取到的EPKey放入url参数中。 107 | ```html 108 |
109 |
110 |
111 |
112 | 113 | 114 |
115 | ``` 116 | 若第一步选择(或后续进行更新)启用reCaptcha,应将reCaptcha token放入`grecaptcha_token`字段提交到服务器,放在form中或拼接在url都可以。 117 | reCaptcha的集成可参考[这里](https://www.cnblogs.com/dulinan/p/12033018.html) 118 | 119 | #### 3. 查询邮件 120 | API: `GET /api/mail/list` 121 | 122 | 请求示例: /api/mail/list?uid=1&pn=1&ps=10 123 | - pn: 分页页码,可不传,从1开始,默认1 124 | - ps:分页页大小,可不传,默认10条 125 | 126 | 响应数据: 127 | ```json 128 | { 129 | "PageNum":1, 130 | "PageSize":10, 131 | "Total":1, 132 | "List": [ 133 | { 134 | "InsertTime":"2020-03-21T16:37:58+08:00", 135 | "State":"STAGING", 136 | "DeliveryTime":"2020-03-21T16:37:58+08:00", 137 | "Content":"
来自用户[小马]的反馈, 用户电话号码: 1999999999, 反馈内容如下:

不错

", 138 | "Raw":{ 139 | "name":"小马", 140 | "contact": "1999999999", 141 | "content": "不错" 142 | } 143 | } 144 | ] 145 | } 146 | ``` 147 | 148 | 字段说明: 149 | - InsertTime: 创建时间 150 | - State: 邮件状态,STAGING: 只保存未投递 DELIVER_SUCCESS: 投递成功 DELIVER_FAILED: 投递失败 151 | - DeliveryTime: 邮件投递时间 152 | - Raw: 对应form中的数据 153 | 154 | ## Tips 155 | 156 | 部署时部分依赖需要科学上网才能下载,若条件不允许,可手动解压[`dep.tar.gz`](https://pan.baidu.com/s/1IJard_GsZJid0WhCHjIF_w)(提取码: jgss)到gopath/src目录中 157 | 158 | License 159 | ============ 160 | ```text 161 | GNU LESSER GENERAL PUBLIC LICENSE 162 | Version 2.1, February 1999 163 | 164 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 165 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 166 | Everyone is permitted to copy and distribute verbatim copies 167 | of this license document, but changing it is not allowed. 168 | 169 | [This is the first released version of the Lesser GPL. It also counts 170 | as the successor of the GNU Library Public License, version 2, hence 171 | the version number 2.1.] 172 | ``` 173 | -------------------------------------------------------------------------------- /handler/shortcut/shortcut.go: -------------------------------------------------------------------------------- 1 | package shortcut 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "errors" 7 | "net/http" 8 | 9 | "GoMailer/app" 10 | "GoMailer/common/db" 11 | "GoMailer/common/utils" 12 | "GoMailer/handler" 13 | "GoMailer/handler/dialer" 14 | "GoMailer/handler/endpoint" 15 | "GoMailer/handler/endpoint/preference" 16 | "GoMailer/handler/endpoint/receiver" 17 | "GoMailer/handler/template" 18 | "GoMailer/handler/user" 19 | "GoMailer/handler/userapp" 20 | ) 21 | 22 | const errInvalidParameter = "invalid parameter" 23 | 24 | func init() { 25 | handler.ShortcutRouter.Methods(http.MethodPost).Handler(app.Handler(shortcut)) 26 | } 27 | 28 | type shortcutVO struct { 29 | User *db.User 30 | App *db.UserApp 31 | Endpoint *struct { 32 | Name string 33 | Dialer *db.Dialer 34 | Receiver []*db.Receiver 35 | Template *db.Template 36 | Preference *db.EndpointPreference 37 | } 38 | } 39 | 40 | // shortcut is a short way to create or update a endpoint 41 | // 1. create user if not registered - required 42 | // 2. create app for user if not created, else update it - required 43 | // 3. check dialer exists or not for user, create dialer when not, update when exists 44 | // 4. create template for user 45 | // 5. create or update endpoint - required 46 | // 6. create or update preference for endpoint 47 | // 7. add or update receiver for endpoint 48 | func shortcut(w http.ResponseWriter, r *http.Request) (interface{}, *app.Error) { 49 | vo := &shortcutVO{} 50 | aerr := app.JsonUnmarshalFromRequest(r, vo) 51 | if aerr != nil { 52 | return nil, aerr 53 | } 54 | 55 | if vo.User == nil || vo.Endpoint == nil || vo.App == nil { 56 | return nil, app.Errorf(errors.New("find nil value when validate parameter"), errInvalidParameter) 57 | } 58 | 59 | // 1. create user if not registered 60 | // vo.User is required 61 | user, err := handleUser(vo.User) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | // 2. create app for user if not created, else update it 67 | // vo.App is required 68 | userApp, err := handleUserApp(user, vo.App) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | // vo.Endpoint is required 74 | if utils.IsBlankStr(vo.Endpoint.Name) { 75 | return nil, app.Errorf(errors.New("endpoint name can not be empty"), errInvalidParameter) 76 | } 77 | // 3. check dialer exists or not for user, create dialer when not 78 | dialer, err := handleUserDialer(user, vo.Endpoint.Dialer) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | // 4. create template for user 84 | template, err := handleUserTemplate(user, vo.Endpoint.Template) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // 5. create or update endpoint 90 | // vo.Endpoint is required 91 | ep, err := handleEndpoint(vo.Endpoint.Name, user, userApp, dialer, template) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | // 6. create or update preference for endpoint 97 | _, err = handleEndPointPreference(ep, vo.Endpoint.Preference) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | // 7. add or update receiver for endpoint 103 | err = handleEndPointReceiver(ep, user, userApp, vo.Endpoint.Receiver) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | return struct { 109 | EPKey string 110 | }{EPKey: ep.Key}, nil 111 | } 112 | 113 | func handleEndPointReceiver(ep *db.Endpoint, u *db.User, ua *db.UserApp, r []*db.Receiver) *app.Error { 114 | if len(r) == 0 { 115 | return nil 116 | } 117 | 118 | err := receiver.DeleteByEndpoint(ep.Id) 119 | if err != nil { 120 | return app.Errorf(err, "failed to delete all receiver for endpoint receiver update") 121 | } 122 | for _, r := range r { 123 | r.EndpointId = ep.Id 124 | r.UserId = u.Id 125 | r.UserAppId = ua.Id 126 | } 127 | err = receiver.PatchCreate(r) 128 | if err != nil { 129 | return app.Errorf(err, "failed to create receiver for endpoint") 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func handleEndPointPreference(ep *db.Endpoint, p *db.EndpointPreference) (*db.EndpointPreference, *app.Error) { 136 | if p == nil { 137 | return nil, nil 138 | } 139 | 140 | p.EndpointId = ep.Id 141 | pre, err := preference.FindByEndpoint(ep.Id) 142 | if err != nil { 143 | return nil, app.Errorf(err, "failed to find endpoint preference") 144 | } 145 | if pre == nil { 146 | pre, err = preference.Create(p) 147 | if err != nil { 148 | return nil, app.Errorf(err, "failed to create endpoint preference") 149 | } 150 | } else { 151 | p.Id = pre.Id 152 | pre, err = preference.Update(p) 153 | if err != nil { 154 | return nil, app.Errorf(err, "failed to update endpoint preference") 155 | } 156 | } 157 | 158 | return pre, nil 159 | } 160 | 161 | func handleEndpoint(name string, u *db.User, ap *db.UserApp, ud *db.Dialer, ut *db.Template) ( 162 | *db.Endpoint, *app.Error) { 163 | if utils.IsBlankStr(name) { 164 | return nil, app.Errorf(errors.New("endpoint name can not be empty"), errInvalidParameter) 165 | } 166 | 167 | ep, err := endpoint.FindByName(ap.Id, name) 168 | if err != nil { 169 | return nil, app.Errorf(err, "failed to find endpoint") 170 | } 171 | 172 | nep := &db.Endpoint{} 173 | nep.Name = name 174 | nep.UserId = u.Id 175 | nep.UserAppId = ap.Id 176 | if ud != nil { 177 | nep.DialerId = ud.Id 178 | } 179 | if ut != nil { 180 | nep.TemplateId = ut.Id 181 | } 182 | if ep == nil { 183 | if ud == nil || ut == nil { 184 | return nil, app.Errorf(err, "dialer and template is required when create endpoint") 185 | } 186 | ep, err = endpoint.Create(nep) 187 | if err != nil { 188 | return nil, app.Errorf(err, "failed to create endpoint") 189 | } 190 | key, err := endpoint.RefreshKey(ep.Id) 191 | if err != nil { 192 | return nil, app.Errorf(err, "failed to generate app key") 193 | } 194 | ep.Key = key 195 | } else { 196 | nep.Id = ep.Id 197 | ep, err = endpoint.Update(nep) 198 | if err != nil { 199 | return nil, app.Errorf(err, "failed to update endpoint") 200 | } 201 | } 202 | 203 | return ep, nil 204 | } 205 | 206 | func handleUserTemplate(u *db.User, t *db.Template) (*db.Template, *app.Error) { 207 | if t == nil { 208 | return nil, nil 209 | } 210 | 211 | t.UserId = u.Id 212 | utemplate, err := template.Create(t) 213 | if err != nil { 214 | return nil, app.Errorf(err, "failed to create template") 215 | } 216 | 217 | return utemplate, nil 218 | } 219 | 220 | func handleUserDialer(u *db.User, d *db.Dialer) (*db.Dialer, *app.Error) { 221 | if d == nil { 222 | return nil, nil 223 | } 224 | 225 | if utils.IsBlankStr(d.Name) { 226 | return nil, app.Errorf(errors.New("dialer name can not be empty"), errInvalidParameter) 227 | } 228 | 229 | d.UserId = u.Id 230 | udialer, err := dialer.FindByName(d.UserId, d.Name) 231 | if err != nil { 232 | return nil, app.Errorf(err, "failed to get user dialer") 233 | } 234 | if udialer == nil { 235 | udialer, err = dialer.Create(d) 236 | if err != nil { 237 | return nil, app.Errorf(err, "failed to create dialer") 238 | } 239 | } else { 240 | d.Id = udialer.Id 241 | udialer, err = dialer.Update(d) 242 | if err != nil { 243 | return nil, app.Errorf(err, "failed to update dialer") 244 | } 245 | } 246 | 247 | return udialer, nil 248 | } 249 | 250 | func handleUserApp(u *db.User, ua *db.UserApp) (*db.UserApp, *app.Error) { 251 | if ua == nil { 252 | return nil, app.Errorf(errors.New("app can not be empty"), errInvalidParameter) 253 | } 254 | if utils.IsBlankStr(ua.Name) { 255 | return nil, app.Errorf(errors.New("app name can not be empty"), errInvalidParameter) 256 | } 257 | ua.UserId = u.Id 258 | 259 | uapp, err := userapp.FindByName(ua.UserId, ua.Name) 260 | if err != nil { 261 | return nil, app.Errorf(err, "failed to get user app") 262 | } 263 | if uapp == nil { 264 | uapp, err = userapp.Create(ua) 265 | if err != nil { 266 | return nil, app.Errorf(err, "failed to create app") 267 | } 268 | } else { 269 | ua.Id = uapp.Id 270 | uapp, err = userapp.Update(ua) 271 | if err != nil { 272 | return nil, app.Errorf(err, "failed to update app") 273 | } 274 | } 275 | 276 | return uapp, nil 277 | } 278 | 279 | func handleUser(u *db.User) (*db.User, *app.Error) { 280 | if utils.IsBlankStr(u.Username) || utils.IsBlankStr(u.Password) { 281 | return nil, app.Errorf(errors.New("username or password can not be empty"), errInvalidParameter) 282 | } 283 | 284 | us, err := user.FindByName(u.Username) 285 | if err != nil { 286 | return nil, app.Errorf(err, "failed to get user") 287 | } 288 | ps := sha256.Sum256([]byte(u.Password)) 289 | u.Password = hex.EncodeToString(ps[:]) 290 | if us == nil { 291 | us, err = user.Create(u) 292 | if err != nil { 293 | return nil, app.Errorf(err, "failed to create user") 294 | } 295 | } else { 296 | if us.Password != u.Password { 297 | return nil, app.Errorf(errors.New("password incorrect"), "wrong password") 298 | } 299 | } 300 | 301 | return us, nil 302 | } 303 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | --------------------------------------------------------------------------------