diff --git a/api/form_handlers.go b/api/form_handlers.go index 134a9c5..5f466cf 100644 --- a/api/form_handlers.go +++ b/api/form_handlers.go @@ -59,11 +59,20 @@ func FormCreate(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { ex, err := common.Untaint(formdata.Expire, cfg.RegDuration) // duration or asap allowed if err != nil { return JsonStatus(c, fiber.StatusForbidden, - "Invalid data: "+err.Error()) + "Invalid expire data: "+err.Error()) } entry.Expire = ex } + if len(formdata.Notify) != 0 { + nt, err := common.Untaint(formdata.Notify, cfg.RegEmail) + if err != nil { + return JsonStatus(c, fiber.StatusForbidden, + "Invalid email address: "+err.Error()) + } + entry.Notify = nt + } + // get url [and zip if there are multiple files] returnUrl := strings.Join([]string{cfg.Url, "form", id}, "/") entry.Url = returnUrl diff --git a/api/mail.go b/api/mail.go new file mode 100644 index 0000000..95e72e3 --- /dev/null +++ b/api/mail.go @@ -0,0 +1,54 @@ +/* +Copyright © 2023 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package api + +import ( + "fmt" + "github.com/tlinden/cenophane/cfg" + "net/smtp" +) + +var mailtpl string = `To: %s\r +From: %s\r +Subject: %s\r +\r +%s\r +` + +/* + Send an email via an external mail gateway. SMTP Auth is + required. Errors may occur with a time delay, like server timeouts + etc. So only call it detached via go routine. +*/ +func Sendmail(c *cfg.Config, recipient string, body string, subject string) error { + // Message. + message := []byte(fmt.Sprintf(mailtpl, recipient, c.Mail.From, subject, body)) + + // Authentication. + auth := smtp.PlainAuth("", c.Mail.From, c.Mail.Password, c.Mail.Server) + + // Sending email. + Log("Trying to send mail to %s via %s:%s with subject %s", + recipient, c.Mail.Server, c.Mail.Port, subject) + err := smtp.SendMail(c.Mail.Server+":"+c.Mail.Port, auth, c.Mail.From, []string{recipient}, []byte(message)) + if err != nil { + return err + } + + return nil +} diff --git a/api/upload_handlers.go b/api/upload_handlers.go index b230264..8a1533e 100644 --- a/api/upload_handlers.go +++ b/api/upload_handlers.go @@ -24,6 +24,7 @@ import ( "github.com/tlinden/cenophane/cfg" "github.com/tlinden/cenophane/common" + "fmt" "os" "path/filepath" "strings" @@ -122,7 +123,7 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { // ok, check if we need to remove a form, if so we do it in the // background. delete error doesn't lead to upload failure, we - // only log it. + // only log it. same applies to mail notification. formid, _ := SessionGetFormId(c) if formid != "" { go func() { @@ -132,6 +133,16 @@ func UploadPost(c *fiber.Ctx, cfg *cfg.Config, db *Db) error { if r.Forms[0].Expire == "asap" { db.Delete(apicontext, formid) } + + // email notification to form creator + if r.Forms[0].Notify != "" { + body := fmt.Sprintf("Upload is available under: %s", returnUrl) + subject := fmt.Sprintf("Upload form %s has been used", formid) + err := Sendmail(cfg, r.Forms[0].Notify, body, subject) + if err != nil { + Log("Failed to send mail: %s", err.Error()) + } + } } } }() diff --git a/cenod.hcl b/cenod.hcl index 1c4e7e9..274f3f0 100644 --- a/cenod.hcl +++ b/cenod.hcl @@ -17,3 +17,10 @@ apicontext = [ # this is the root context with all permissions super = "root" + +mail = { + server = "localhost" + port = "25" + from = "root@localhost" + password = "" +} diff --git a/cfg/config.go b/cfg/config.go index 59e6f15..d720492 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -32,6 +32,13 @@ type Apicontext struct { Key string `koanf:"key"` } +type Mailsettings struct { + Server string `koanf:"server"` + Port string `koanf:"port"` + From string `koanf:"from"` + Password string `koanf:"password"` +} + // holds the whole configs, filled by commandline flags, env and config file type Config struct { // Flags+config file settings @@ -57,10 +64,14 @@ type Config struct { // only settable via config Apicontexts []Apicontext `koanf:"apicontext"` + // smtp settings + Mail Mailsettings `koanf:mail` + // Internals only RegNormalizedFilename *regexp.Regexp RegDuration *regexp.Regexp RegKey *regexp.Regexp + RegEmail *regexp.Regexp CleanInterval time.Duration DefaultExpire int } @@ -107,6 +118,8 @@ func (c *Config) ApplyDefaults() { c.RegNormalizedFilename = regexp.MustCompile(`[^\w\d\-_\.]`) c.RegDuration = regexp.MustCompile(`[^dhms0-9]`) c.RegKey = regexp.MustCompile(`[^a-zA-Z0-9\-]`) + c.RegEmail = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) + c.RegEmail = regexp.MustCompile(`[^a-z0-9._%+\-@0-9]`) c.CleanInterval = 10 * time.Second c.DefaultExpire = 30 * 86400 // 1 month diff --git a/cmd/formtemplate.go b/cmd/formtemplate.go index 5b45e7b..38c8bff 100644 --- a/cmd/formtemplate.go +++ b/cmd/formtemplate.go @@ -12,33 +12,50 @@ const formtemplate = ` File upload form - + + +

Upload form {{ .Id }}

- +
-
-
- - + +
+

+ Use this form to upload one or more files. The creator of the form will automatically get notified. +

-
- - +
+ +
+ +
+ +
+ +
+ +
+
+
+
- - +
+ + diff --git a/templates/formtemplate.html b/templates/formtemplate.html index bdb7a22..b4ac878 100644 --- a/templates/formtemplate.html +++ b/templates/formtemplate.html @@ -22,7 +22,6 @@
-

Use this form to upload one or more files. The creator of the form will automatically get notified. @@ -72,8 +71,8 @@ $('.statusMsg').html(''); if(response.success){ $('#UploadForm')[0].reset(); - $('.statusMsg').html('

Your upload is available at here for download

'); + $('.statusMsg').html('

Your upload is available at ' + +response.uploads[0].url+' for download

'); $('#UploadForm').hide(); }else{ $('.statusMsg').html('

'+response.message+'

'); diff --git a/upctl/cfg/config.go b/upctl/cfg/config.go index 8be2fb4..c283ba4 100644 --- a/upctl/cfg/config.go +++ b/upctl/cfg/config.go @@ -46,6 +46,7 @@ type Config struct { // required for forms Description string + Notify string } func Getversion() string { diff --git a/upctl/cmd/formcommands.go b/upctl/cmd/formcommands.go index 8953791..e655d8c 100644 --- a/upctl/cmd/formcommands.go +++ b/upctl/cmd/formcommands.go @@ -64,7 +64,7 @@ func FormCreateCommand(conf *cfg.Config) *cobra.Command { // options formCreateCmd.PersistentFlags().StringVarP(&conf.Expire, "expire", "e", "", "Expire setting: asap or duration (accepted shortcuts: dmh)") formCreateCmd.PersistentFlags().StringVarP(&conf.Description, "description", "D", "", "Description of the form") - formCreateCmd.PersistentFlags().StringVarP(&conf.Description, "notify", "n", "", "Email address to get notified when consumer has uploaded files") + formCreateCmd.PersistentFlags().StringVarP(&conf.Notify, "notify", "n", "", "Email address to get notified when consumer has uploaded files") formCreateCmd.Aliases = append(formCreateCmd.Aliases, "add") formCreateCmd.Aliases = append(formCreateCmd.Aliases, "+") diff --git a/upctl/lib/client.go b/upctl/lib/client.go index 0d170ff..0e2309d 100644 --- a/upctl/lib/client.go +++ b/upctl/lib/client.go @@ -50,7 +50,7 @@ type ListParams struct { Apicontext string `json:"apicontext"` } -const Maxwidth = 10 +const Maxwidth = 12 /* Create a new request object for outgoing queries @@ -142,15 +142,15 @@ func HandleResponse(c *cfg.Config, resp *req.Response) error { fmt.Println(trace) } - if !resp.IsSuccessState() { - return fmt.Errorf("bad response: %s", resp.Status) - } - if err := json.Unmarshal([]byte(resp.String()), &r); err != nil { // text output! r.Message = resp.String() } + if !resp.IsSuccessState() { + return fmt.Errorf("bad response: %s (%s)", resp.Status, r.Message) + } + if !r.Success { if len(r.Message) == 0 { if resp.Err != nil { @@ -331,6 +331,7 @@ func CreateForm(w io.Writer, c *cfg.Config) error { SetFormData(map[string]string{ "expire": c.Expire, "description": c.Description, + "notify": c.Notify, }). Post(rq.Url) diff --git a/upctl/lib/output.go b/upctl/lib/output.go index 1590769..a12fefa 100644 --- a/upctl/lib/output.go +++ b/upctl/lib/output.go @@ -95,6 +95,7 @@ func WriteExtended(w io.Writer, response *common.Response) { fmt.Fprintf(w, format, "Context", entry.Context) fmt.Fprintf(w, format, "Created", entry.Created) fmt.Fprintf(w, format, "Description", entry.Description) + fmt.Fprintf(w, format, "Notify", entry.Notify) fmt.Fprintf(w, format, "Url", entry.Url) fmt.Fprintln(w) }