diff --git a/email.go b/email.go index 3a8f208..c22c73b 100644 --- a/email.go +++ b/email.go @@ -158,6 +158,38 @@ func (email *Emailer) constructCreated(code, username, address string, invite In return nil } +func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error { + email.content.subject = ctx.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin") + d, t, expires_in := email.formatExpiry(pwr.Expiry, true, ctx.datePattern, ctx.timePattern) + message := ctx.config.Section("email").Key("message").String() + for _, key := range []string{"html", "text"} { + fpath := ctx.config.Section("password_resets").Key("email_" + key).String() + tpl, err := template.ParseFiles(fpath) + if err != nil { + return err + } + var tplData bytes.Buffer + err = tpl.Execute(&tplData, map[string]string{ + "username": pwr.Username, + "expiry_date": d, + "expiry_time": t, + "expires_in": expires_in, + "pin": pwr.Pin, + "message": message, + }) + if err != nil { + return err + } + if key == "html" { + email.content.html = tplData.String() + } else { + email.content.text = tplData.String() + } + } + email.sendType = "reset" + return nil +} + func (email *Emailer) send(address string, ctx *appContext) error { if email.sendMethod == "mailgun" { message := email.mg.NewMessage( diff --git a/go.mod b/go.mod index 31834d0..15db064 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.13.0 github.com/flosch/pongo2 v0.0.0-20200529170236-5abacdfa4915 + github.com/fsnotify/fsnotify v1.4.9 github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 github.com/gin-gonic/gin v1.6.3 github.com/go-playground/validator/v10 v10.3.0 // indirect diff --git a/main.go b/main.go index e884be6..83e3836 100644 --- a/main.go +++ b/main.go @@ -197,6 +197,10 @@ func main() { inviteDaemon := NewRepeater(time.Duration(60*time.Second), ctx) go inviteDaemon.Run() + if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { + ctx.StartPWR() + } + ctx.info.Println("Loading routes") router := gin.New() diff --git a/passwordreset4eb59133-d0d4-4177-bb54-48e3741c5788.json b/passwordreset4eb59133-d0d4-4177-bb54-48e3741c5788.json new file mode 100644 index 0000000..60fac4b --- /dev/null +++ b/passwordreset4eb59133-d0d4-4177-bb54-48e3741c5788.json @@ -0,0 +1 @@ +{"Pin":"93-6C-1C-EA","UserName":"Harvey","PinFile":"/config/passwordreset4eb59133-d0d4-4177-bb54-48e3741c5788.json","ExpirationDate":"2020-08-01T15:31:36.4133258Z"} \ No newline at end of file diff --git a/pwreset.go b/pwreset.go new file mode 100644 index 0000000..3b792dc --- /dev/null +++ b/pwreset.go @@ -0,0 +1,92 @@ +package main + +import ( + "encoding/json" + "github.com/fsnotify/fsnotify" + "io/ioutil" + "os" + "strings" + "time" +) + +func (ctx *appContext) StartPWR() { + ctx.info.Println("Starting password reset daemon") + path := ctx.config.Section("password_resets").Key("watch_directory").String() + if _, err := os.Stat(path); os.IsNotExist(err) { + ctx.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path) + return + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + ctx.err.Printf("Couldn't initialise password reset daemon") + return + } + defer watcher.Close() + + done := make(chan bool) + go pwrMonitor(ctx, watcher) + err = watcher.Add(path) + if err != nil { + ctx.err.Printf("Failed to start password reset daemon: %s", err) + } + <-done +} + +type Pwr struct { + Pin string `json:"Pin"` + Username string `json:"UserName"` + Expiry time.Time `json:"ExpirationDate"` +} + +func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Op&fsnotify.Write == fsnotify.Write && strings.Contains(event.Name, "passwordreset") { + var pwr Pwr + data, err := ioutil.ReadFile(event.Name) + if err != nil { + return + } + err = json.Unmarshal(data, &pwr) + if len(pwr.Pin) == 0 || err != nil { + return + } + ctx.info.Printf("New password reset for user \"%s\"", pwr.Username) + if ct := time.Now(); pwr.Expiry.After(ct) { + user, status, err := ctx.jf.userByName(pwr.Username, false) + if !(status == 200 || status == 204) || err != nil { + ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) + ctx.debug.Printf("Error: %s", err) + return + } + ctx.storage.loadEmails() + address, ok := ctx.storage.emails[user["Id"].(string)].(string) + if !ok { + ctx.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username) + return + } + if ctx.email.constructReset(pwr, ctx) != nil { + ctx.err.Printf("Failed to construct password reset email for %s", pwr.Username) + } else if ctx.email.send(address, ctx) != nil { + ctx.err.Printf("Failed to send password reset email to \"%s\"", address) + } else { + ctx.info.Printf("Sent password reset email to \"%s\"", address) + } + } else { + ctx.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry) + } + + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + ctx.err.Printf("Password reset daemon: %s", err) + } + } +}