diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | db.go | 30 | ||||
-rw-r--r-- | formdec.go | 39 | ||||
-rw-r--r-- | mailing.go | 69 | ||||
-rw-r--r-- | mailremind.ini | 23 | ||||
-rw-r--r-- | mails.go | 52 | ||||
-rw-r--r-- | mails/activationcode.tpl | 9 | ||||
-rw-r--r-- | main.go | 59 | ||||
-rw-r--r-- | register.go | 101 | ||||
-rw-r--r-- | timelocs.go | 42 | ||||
-rw-r--r-- | tpls.go | 28 | ||||
-rw-r--r-- | tpls/master.tpl | 12 | ||||
-rw-r--r-- | tpls/register.tpl | 21 |
13 files changed, 486 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bdd5b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +mailremind @@ -0,0 +1,30 @@ +package main + +import ( + "kch42.de/gostuff/mailremind/model" + "log" +) + +var db model.DBInfo +var dbcon model.DBCon + +func initDB() { + dbdrv, err := conf.GetString("db", "driver") + if err != nil { + log.Fatalf("Could not get db.driver from config: %s", err) + } + + dbconf, err := conf.GetString("db", "conf") + if err != nil { + log.Fatalf("Could not get db.conf from config: %s", err) + } + + var ok bool + if db, ok = model.GetDBInfo(dbdrv); !ok { + log.Fatalf("Could not get info for dbdrv %s: %s", dbdrv, err) + } + + if dbcon, err = db.Connect(dbconf); err != nil { + log.Fatalf("Unable to connect to %s database: %s", dbdrv, err) + } +} diff --git a/formdec.go b/formdec.go new file mode 100644 index 0000000..d554696 --- /dev/null +++ b/formdec.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/gorilla/schema" + "reflect" + "regexp" + "time" +) + +type EMail string + +var emailRegex = regexp.MustCompile(`^.+@.+$`) + +func EMailConvert(s string) reflect.Value { + if emailRegex.MatchString(s) { + return reflect.ValueOf(EMail(s)) + } + return reflect.Value{} +} + +type timelocForm struct { + Loc *time.Location +} + +func locationConverter(s string) reflect.Value { + loc, err := time.LoadLocation(s) + if err != nil { + return reflect.Value{} + } + return reflect.ValueOf(timelocForm{loc}) +} + +var formdec *schema.Decoder + +func init() { + formdec = schema.NewDecoder() + formdec.RegisterConverter(EMail(""), EMailConvert) + formdec.RegisterConverter(timelocForm{}, locationConverter) +} diff --git a/mailing.go b/mailing.go new file mode 100644 index 0000000..a70138e --- /dev/null +++ b/mailing.go @@ -0,0 +1,69 @@ +package main + +import ( + "kch42.de/gostuff/mailremind/mailing" + "log" +) + +var MailFrom string + +type email struct { + To, From string + Msg []byte + OK chan<- bool +} + +var mailchan chan *email + +func Mail(to, from string, msg []byte) bool { + ok := make(chan bool) + mailchan <- &email{to, from, msg, ok} + return <-ok +} + +func initMailing() { + meth, err := conf.GetString("mail", "method") + if err != nil { + log.Fatalf("Could not get mail.method from config: %s", err) + } + + MailFrom, err = conf.GetString("mail", "addr") + if err != nil { + log.Fatalf("Could not get mail.addr from config: %s", err) + } + + parallel, err := conf.GetInt("mail", "parallel") + if err != nil { + log.Fatalf("Could not get mail.parallel from config: %s", err) + } + + if parallel <= 0 { + log.Fatalln("mail.parallel must be > 0") + } + + mailchan = make(chan *email) + + mc, ok := mailing.MailersByName[meth] + if !ok { + log.Fatalf("Unknown mail method: %s", meth) + } + + for i := int64(0); i < parallel; i++ { + mailer, err := mc(conf) + if err != nil { + log.Fatalf("Error while initializing mail: %s", err) + } + + go func(mailer mailing.Mailer) { + for { + mail := <-mailchan + if err := mailer.Mail(mail.To, mail.From, mail.Msg); err != nil { + log.Printf("Could not send mail to \"%s\": %s", mail.To, err) + mail.OK <- false + } else { + mail.OK <- true + } + } + }(mailer) + } +} diff --git a/mailremind.ini b/mailremind.ini new file mode 100644 index 0000000..2331919 --- /dev/null +++ b/mailremind.ini @@ -0,0 +1,23 @@ +[web] +baseurl=http://localhost:8080 + +[net] +laddr=:8080 + +[paths] +static=static +tpls=tpls +mailtpls=mails + +[db] +driver=mysql +conf=mailremind:mailremind@tcp/mailremind + +[mail] +method=sendmail +addr=nobody@kch42.net +parallel=10 +exec=msmtp +arg1=-a +arg2=kch42 + diff --git a/mails.go b/mails.go new file mode 100644 index 0000000..5e5a6dc --- /dev/null +++ b/mails.go @@ -0,0 +1,52 @@ +package main + +import ( + "bytes" + "fmt" + "kch42.de/gostuff/mailremind/model" + "log" + "path" + "text/template" + "time" +) + +func loadMailTpl(tplroot, name string) *template.Template { + tpl, err := template.ParseFiles(path.Join(tplroot, name+".tpl")) + if err != nil { + log.Fatalf("Could not load mailtemplate %s: %s", name, err) + } + return tpl +} + +var mailActivationcode *template.Template + +func initMails() { + tplroot, err := conf.GetString("paths", "mailtpls") + if err != nil { + log.Fatalf("Could not get paths.mailtpls from config: %s", err) + } + + mailActivationcode = loadMailTpl(tplroot, "activationcode") +} + +type activationcodeData struct { + URL string +} + +func SendActivationcode(to, acCode string, uid model.DBID) bool { + buf := new(bytes.Buffer) + fmt.Fprintf(buf, "To: %s\n", to) + fmt.Fprintf(buf, "From: %s\n", MailFrom) + fmt.Fprintf(buf, "Subject: Activation code for your mailremind account\n") + fmt.Fprintf(buf, "Date: %s\n", time.Now().Format(time.RFC822)) + + fmt.Fprintln(buf, "") + + url := fmt.Sprintf("%s/activate/U=%s&Code=%s", baseurl, uid, acCode) + if err := mailActivationcode.Execute(buf, activationcodeData{url}); err != nil { + log.Printf("Error while executing mail template (activationcode): %s", err) + return false + } + + return Mail(to, MailFrom, buf.Bytes()) +} diff --git a/mails/activationcode.tpl b/mails/activationcode.tpl new file mode 100644 index 0000000..c31e77e --- /dev/null +++ b/mails/activationcode.tpl @@ -0,0 +1,9 @@ +Hi, + +Your mailremind account was successfully created. +You can activate it with this link: + + {{.URL}} + +If you didn't register a mailremind account, you can simply ignore +this message. @@ -0,0 +1,59 @@ +package main + +import ( + "flag" + "fmt" + "github.com/gorilla/mux" + "github.com/kch42/simpleconf" + _ "kch42.de/gostuff/mailremind/model/mysql" + "log" + "net/http" +) + +func debug(rw http.ResponseWriter, req *http.Request) { + fmt.Fprintf(rw, "Content-Type: text/plain\r\n\r\n%#v", req) +} + +var conf simpleconf.Config +var baseurl string + +func main() { + confpath := flag.String("config", "", "Path to config file") + flag.Parse() + + var err error + if conf, err = simpleconf.LoadByFilename(*confpath); err != nil { + log.Fatalf("Could not read config: %s", err) + } + + if baseurl, err = conf.GetString("web", "baseurl"); err != nil { + log.Fatalf("Could not get web.baseurl from config: %s", err) + } + + initTpls() + loadTimeLocs() + initMailing() + initMails() + initDB() + defer dbcon.Close() + + staticpath, err := conf.GetString("paths", "static") + if err != nil { + log.Fatalf("Could not get paths.static config: %s", err) + } + + laddr, err := conf.GetString("net", "laddr") + if err != nil { + log.Fatalf("Could not get net.laddr config: %s", err) + } + + router := mux.NewRouter() + router.PathPrefix("/static").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticpath)))) + router.HandleFunc("/register", register) + + http.Handle("/", router) + + if err := http.ListenAndServe(laddr, nil); err != nil { + log.Fatalf("Could not ListenAndServe: %s", err) + } +} diff --git a/register.go b/register.go new file mode 100644 index 0000000..936f540 --- /dev/null +++ b/register.go @@ -0,0 +1,101 @@ +package main + +import ( + "code.google.com/p/go.crypto/bcrypt" + "kch42.de/gostuff/mailremind/model" + "log" + "math/rand" + "net/http" +) + +type registerData struct { + Error, Success string + Timezones *[]string +} + +type registerFormdata struct { + Mail EMail + Password, RetypePassword string + Timezone timelocForm +} + +var acCodeAlphabet = []rune("qwertzuiopasdfghjklyxcvbnmQWERTZUIOPASDFGHJKLYXCVBNM1234567890") + +func genAcCode() string { + const codelen = 10 + alphalen := len(acCodeAlphabet) + + code := make([]rune, codelen) + for i := 0; i < codelen; i++ { + code[i] = acCodeAlphabet[rand.Intn(alphalen)] + } + + return string(code) +} + +func register(rw http.ResponseWriter, req *http.Request) { + outdata := ®isterData{Timezones: &timeLocs} + defer func() { + if err := tplRegister.Execute(rw, outdata); err != nil { + log.Printf("Exec tplRegister: %s", err) + } + }() + + if req.Method == "POST" { + if err := req.ParseForm(); err != nil { + outdata.Error = "Data of form could not be understand. If this happens again, please contact support!" + return + } + + indata := new(registerFormdata) + if err := formdec.Decode(indata, req.Form); (err != nil) || (indata.Mail == "") || (indata.Timezone.Loc == nil) { + outdata.Error = "Input data wrong or missing. Please fill in all values and make sure to provide a valid E-Mail address." + return + } + + if indata.Password == "" { + outdata.Error = "Empty passwords are not allowed." + return + } + + if indata.Password != indata.RetypePassword { + outdata.Error = "Passwords are not identical." + return + } + + mail := string(indata.Mail) + + switch _, err := dbcon.UserByMail(mail); err { + case nil: + outdata.Error = "This E-Mail address is already used." + return + case model.NotFound: + default: + log.Printf("Error while checking, if mail is used: %s", err) + outdata.Error = "Internal error, sorry. If this happens again, please contact support!" + return + } + + acCode := genAcCode() + pwhash, err := bcrypt.GenerateFromPassword([]byte(indata.Password), bcrypt.DefaultCost) + if err != nil { + log.Printf("Error while hashing password: %s", err) + outdata.Error = "Internal error, sorry. If this happens again, please contact support!" + return + } + + user, err := dbcon.AddUser(mail, pwhash, indata.Timezone.Loc, false, acCode) + if err != nil { + log.Printf("Could not create user (%s): %s", indata.Mail, err) + outdata.Error = "Internal error, sorry. If this happens again, please contact support!" + return + } + + if !SendActivationcode(mail, acCode, user.ID()) { + outdata.Error = "We could not send you a mail with your confirmation code." + return + } + + outdata.Success = "Account created successfully! We sent you an E-Mail that contains a link to activate your account." + } +} diff --git a/timelocs.go b/timelocs.go new file mode 100644 index 0000000..bb04070 --- /dev/null +++ b/timelocs.go @@ -0,0 +1,42 @@ +package main + +import ( + "archive/zip" + "log" + "path" + "runtime" + "sort" + "sync" +) + +var timeLocs []string +var tlOnce sync.Once + +func listTimeLocations() ([]string, error) { + zoneinfoZip := path.Join(runtime.GOROOT(), "lib", "time", "zoneinfo.zip") + z, err := zip.OpenReader(zoneinfoZip) + if err != nil { + return nil, err + } + defer z.Close() + + locs := []string{} + for _, f := range z.File { + if f.Name[len(f.Name)-1] == '/' { + continue + } + locs = append(locs, f.Name) + } + + sort.Strings(locs) + return locs, nil +} + +func loadTimeLocs() { + tlOnce.Do(func() { + var err error + if timeLocs, err = listTimeLocations(); err != nil { + log.Fatalf("Could not load time locations: %s", err) + } + }) +} @@ -0,0 +1,28 @@ +package main + +import ( + "html/template" + "log" + "path" +) + +func loadTpl(tplpath, name string) *template.Template { + tpl, err := template.ParseFiles( + path.Join(tplpath, "master.tpl"), + path.Join(tplpath, name+".tpl")) + if err != nil { + log.Fatalf("Could not load template \"%s\": %s", name, err) + } + return tpl +} + +var tplRegister *template.Template + +func initTpls() { + tplpath, err := conf.GetString("paths", "tpls") + if err != nil { + log.Fatalf("Could not get paths.tpls config: %s", err) + } + + tplRegister = loadTpl(tplpath, "register") +} diff --git a/tpls/master.tpl b/tpls/master.tpl new file mode 100644 index 0000000..b675598 --- /dev/null +++ b/tpls/master.tpl @@ -0,0 +1,12 @@ +<html> +<head> + <title>{{template "title"}} – mailremind</title> +</head> +<body> + <h1>{{template "title"}}</h1> + + <div class="content"> + {{template "content" .}} + </div> +</body> +</html>
\ No newline at end of file diff --git a/tpls/register.tpl b/tpls/register.tpl new file mode 100644 index 0000000..a6e0c38 --- /dev/null +++ b/tpls/register.tpl @@ -0,0 +1,21 @@ +{{define "title"}}Register{{end}} + +{{define "content"}} + {{if .Success}} + <div class="success">{{.Success}}</div> + {{else}} + {{if .Error}}<div class="error">{{.Error}}</div>{{end}} + <form action="/register" method="post" accept-charset="UTF-8"> + <p><strong>E-Mail:</strong> <input type="text" name="Mail" /></p> + <p><strong>Password:</strong> <input type="password" name="Password" /></p> + <p><strong>Retype Password:</strong> <input type="password" name="RetypePassword" /></p> + <p> + <strong>Timezone:</strong> + <select size="0" name="Timezone"> + {{range .Timezones}}<option value="{{.}}">{{.}}</option>{{end}} + </select> + </p> + <p><input type="submit" /></p> + </form> + {{end}} +{{end}}
\ No newline at end of file |