summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--db.go30
-rw-r--r--formdec.go39
-rw-r--r--mailing.go69
-rw-r--r--mailremind.ini23
-rw-r--r--mails.go52
-rw-r--r--mails/activationcode.tpl9
-rw-r--r--main.go59
-rw-r--r--register.go101
-rw-r--r--timelocs.go42
-rw-r--r--tpls.go28
-rw-r--r--tpls/master.tpl12
-rw-r--r--tpls/register.tpl21
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
diff --git a/db.go b/db.go
new file mode 100644
index 0000000..ccddd00
--- /dev/null
+++ b/db.go
@@ -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.
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..9eaebcc
--- /dev/null
+++ b/main.go
@@ -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 := &registerData{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)
+ }
+ })
+}
diff --git a/tpls.go b/tpls.go
new file mode 100644
index 0000000..e7298fa
--- /dev/null
+++ b/tpls.go
@@ -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