Skip to content

Commit

Permalink
Make providing name in subscriber creation optional and as#te…
Browse files Browse the repository at this point in the history
…rnally. Closes #1630.
  • Loading branch information
knadh committed Dec 30, 2023
1 parent a9a7156 commit 0d74619
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 51 deletions.
20 changes: 3 additions & 17 deletions cmd/subscribers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strconv"
"strings"

"github.com/knadh/listmonk/internal/subimporter"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
)
Expand Down Expand Up @@ -183,12 +184,7 @@ loop:
func handleCreateSubscriber(c echo.Context) error {
var (
app = c.Get("app").(*App)
req struct {
models.Subscriber
Lists []int `json:"lists"`
ListUUIDs []string `json:"list_uuids"`
PreconfirmSubs bool `json:"preconfirm_subscriptions"`
}
req subimporter.SubReq
)

// Get and validate fields.
Expand All @@ -197,20 +193,10 @@ func handleCreateSubscriber(c echo.Context) error {
}

// Validate fields.
if len(req.Email) > 1000 {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidEmail"))
}

em, err := app.importer.SanitizeEmail(req.Email)
req, err := app.importer.ValidateFields(req)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
req.Email = em

req.Name = strings.TrimSpace(req.Name)
if len(req.Name) == 0 || len(req.Name) > stdInputMaxLen {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
}

// Insert the subscriber into the DB.
sub, _, err := app.core.InsertSubscriber(req.Subscriber, req.Lists, req.ListUUIDs, req.PreconfirmSubs)
Expand Down
9 changes: 9 additions & 0 deletions cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,12 @@ func strSliceContains(str string, sl []string) bool {
func trimNullBytes(b []byte) string {
return string(bytes.Trim(b, "\x00"))
}

func titleCase(input string) string {
parts := strings.Fields(input)
for n, p := range parts {
parts[n] = strings.Title(p)
}

return strings.Join(parts, " ")
}
12 changes: 0 additions & 12 deletions frontend/src/views/SubscriberForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -222,18 +222,6 @@ export default Vue.extend({
},
onSubmit() {
// If there is no name, auto-generate one from the e-mail.
if (!this.form.name) {
let name = '';
[name] = this.form.email.toLowerCase().split('@');
if (name.includes('.')) {
this.form.name = name.split('.').map((c) => this.$utils.titleCase(c)).join(' ');
} else {
this.form.name = this.$utils.titleCase(name);
}
}
if (this.isEditing) {
this.updateSubscriber();
return;
Expand Down
51 changes: 29 additions & 22 deletions internal/subimporter/importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ type Status struct {
// SubReq is a wrapper over the Subscriber model.
type SubReq struct {
models.Subscriber
Lists pq.Int64Array `json:"lists"`
ListUUIDs pq.StringArray `json:"list_uuids"`
PreconfirmSubs bool `json:"preconfirm_subscriptions"`
Lists []int `json:"lists"`
ListUUIDs []string `json:"list_uuids"`
PreconfirmSubs bool `json:"preconfirm_subscriptions"`
}

type importStatusTpl struct {
Expand Down Expand Up @@ -263,11 +263,11 @@ func (s *Session) Start() {
total = 0
cur = 0

listIDs = make(pq.Int64Array, len(s.opt.ListIDs))
listIDs = make([]int, len(s.opt.ListIDs))
)

for i, v := range s.opt.ListIDs {
listIDs[i] = int64(v)
listIDs[i] = v
}

for sub := range s.subQueue {
Expand All @@ -294,7 +294,7 @@ func (s *Session) Start() {
}

if s.opt.Mode == ModeSubscribe {
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, listIDs, s.opt.SubStatus, s.opt.Overwrite)
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs, pq.Array(listIDs), s.opt.SubStatus, s.opt.Overwrite)
} else if s.opt.Mode == ModeBlocklist {
_, err = stmt.Exec(uu, sub.Email, sub.Name, sub.Attribs)
}
Expand Down Expand Up @@ -324,7 +324,7 @@ func (s *Session) Start() {
if cur == 0 {
s.im.setStatus(StatusFinished)
s.log.Printf("imported finished")
if _, err := s.im.opt.UpdateListDateStmt.Exec(listIDs); err != nil {
if _, err := s.im.opt.UpdateListDateStmt.Exec(pq.Array(listIDs)); err != nil {
s.log.Printf("error updating lists date: %v", err)
}
s.im.sendNotif(StatusFinished)
Expand All @@ -343,7 +343,7 @@ func (s *Session) Start() {
s.im.incrementImportCount(cur)
s.im.setStatus(StatusFinished)
s.log.Printf("imported finished")
if _, err := s.im.opt.UpdateListDateStmt.Exec(listIDs); err != nil {
if _, err := s.im.opt.UpdateListDateStmt.Exec(pq.Array(listIDs)); err != nil {
s.log.Printf("error updating lists date: %v", err)
}
s.im.sendNotif(StatusFinished)
Expand Down Expand Up @@ -486,15 +486,11 @@ func (s *Session) LoadCSV(srcPath string, delim rune) error {
}

hdrKeys := s.mapCSVHeaders(csvHdr, csvHeaders)
// email, and name are required headers.
// email is a required header.
if _, ok := hdrKeys["email"]; !ok {
s.log.Printf("'email' column not found in '%s'", srcPath)
return errors.New("'email' column not found")
}
if _, ok := hdrKeys["name"]; !ok {
s.log.Printf("'name' column not found in '%s'", srcPath)
return errors.New("'name' column not found")
}

var (
lnHdr = len(hdrKeys)
Expand Down Expand Up @@ -541,9 +537,12 @@ func (s *Session) LoadCSV(srcPath string, delim rune) error {

sub := SubReq{}
sub.Email = row["email"]
sub.Name = row["name"]

sub, err = s.im.validateFields(sub)
if v, ok := row["name"]; ok {
sub.Name = v
}

sub, err = s.im.ValidateFields(sub)
if err != nil {
s.log.Printf("skipping line %d: %s: %v", i, sub.Email, err)
continue
Expand Down Expand Up @@ -630,23 +629,31 @@ func (im *Importer) SanitizeEmail(email string) (string, error) {
return em.Address, nil
}

// validateFields validates incoming subscriber field values and returns sanitized fields.
func (im *Importer) validateFields(s SubReq) (SubReq, error) {
// ValidateFields validates incoming subscriber field values and returns sanitized fields.
func (im *Importer) ValidateFields(s SubReq) (SubReq, error) {
if len(s.Email) > 1000 {
return s, errors.New(im.i18n.T("subscribers.invalidEmail"))
}

s.Name = strings.TrimSpace(s.Name)
if len(s.Name) == 0 || len(s.Name) > stdInputMaxLen {
return s, errors.New(im.i18n.T("subscribers.invalidName"))
}

em, err := im.SanitizeEmail(s.Email)
if err != nil {
return s, err
}
s.Email = strings.ToLower(em)

// If there's no name, use the name part of the e-mail.
s.Name = strings.TrimSpace(s.Name)
if len(s.Name) == 0 {
name := strings.ToLower(strings.Split(s.Email, "@")[0])

parts := strings.Fields(strings.ReplaceAll(name, ".", " "))
for n, p := range parts {
parts[n] = strings.Title(p)
}

s.Name = strings.Join(parts, " ")
}

return s, nil
}

Expand Down

0 comments on commit 0d74619

Please # to comment.