├── .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": "
{{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 | 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":"不错
", 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 |