Loading folders/messages.go +80 −0 Original line number Diff line number Diff line Loading @@ -2,14 +2,36 @@ package folders import ( "errors" "fmt" "time" "unsafe" "git.lyda.ie/pp/bulletin/storage" "git.lyda.ie/pp/bulletin/this" ) // MaxMessageSize is the maximum size of a message body in bytes (64 KB). const MaxMessageSize = 64 * 1024 // MaxSubjectSize is the maximum size of a subject in bytes. const MaxSubjectSize = 256 // ValidateMessageSize checks that a message and subject are within size limits. func ValidateMessageSize(subject, message string) error { if len(subject) > MaxSubjectSize { return fmt.Errorf("subject too long (%d bytes, max %d)", len(subject), MaxSubjectSize) } if len(message) > MaxMessageSize { return fmt.Errorf("message too long (%d bytes, max %d)", len(message), MaxMessageSize) } return nil } // CreateMessage creates a new message. func CreateMessage(author, subject, message, folder string, permanent, system, shutdown int, expiration *time.Time) error { if err := ValidateMessageSize(subject, message); err != nil { return err } ctx := storage.Context() if expiration == nil { days, err := this.Q.GetFolderExpire(ctx, folder) Loading Loading @@ -173,6 +195,64 @@ func ListMessages(folder string) ([]storage.Message, error) { return rows, err } // dirRow is the interface for Dir query row types that have the same // fields as Message. type dirRow interface { storage.DirMessagesRow | storage.DirMessagesSeenRow | storage.DirMessagesUnseenRow | storage.DirMessagesMarkedRow | storage.DirMessagesUnmarkedRow | storage.DirSearchRow | storage.DirSearchSubjectRow | storage.DirSearchReplyRow } // ToMessages converts Dir query rows to Messages. Exported for use in repl. func ToMessages[T dirRow](rows []T) []storage.Message { return toMessages(rows) } func toMessages[T dirRow](rows []T) []storage.Message { msgs := make([]storage.Message, len(rows)) for i := range rows { msgs[i] = *(*storage.Message)(unsafe.Pointer(&rows[i])) // #nosec G103 -- types are layout-identical } return msgs } // DirMessages lists messages without message bodies. func DirMessages(folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessages(ctx, folder) return toMessages(rows), err } // DirMessagesSeen lists seen messages without message bodies. func DirMessagesSeen(login, folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessagesSeen(ctx, folder, login) return toMessages(rows), err } // DirMessagesUnseen lists unseen messages without message bodies. func DirMessagesUnseen(login, folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessagesUnseen(ctx, folder, login) return toMessages(rows), err } // DirMessagesMarked lists marked messages without message bodies. func DirMessagesMarked(login, folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessagesMarked(ctx, folder, login) return toMessages(rows), err } // DirMessagesUnmarked lists unmarked messages without message bodies. func DirMessagesUnmarked(login, folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessagesUnmarked(ctx, folder, login) return toMessages(rows), err } // WroteAllMessages returns true if login wrote all msgids func WroteAllMessages(login string, msgids []int64) bool { ctx := storage.Context() Loading key/key.go +27 −9 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ import ( "os" "path" "strings" "time" "git.lyda.ie/pp/bulletin/storage" "github.com/adrg/xdg" Loading Loading @@ -60,14 +61,25 @@ func addToFile(login, public string) error { } defer unix.Flock(fd, unix.LOCK_UN) //nolint:errcheck // unlock after we're done // Check for duplicates. keycontent, err := io.ReadAll(f) // Check for duplicates using a scanner. newKeyBytes := theKey.Marshal() scanner := bufio.NewScanner(f) for scanner.Scan() { line := bytes.TrimSpace(scanner.Bytes()) if len(line) == 0 { continue } existing, _, _, _, err := ssh.ParseAuthorizedKey(line) if err != nil { return err continue } if bytes.Contains(keycontent, ssh.MarshalAuthorizedKey(theKey)) { if bytes.Equal(existing.Marshal(), newKeyBytes) { return ErrDuplicateKey } } if err := scanner.Err(); err != nil { return err } // Generate and write the key. keyline := fmt.Sprintf(keytemplate, bulletin, login, string(ssh.MarshalAuthorizedKey(theKey))) Loading Loading @@ -225,16 +237,22 @@ func FetchDB(q *storage.Queries, login, nickname, username string) string { if site == "" { return fmt.Sprintln("ERROR: site nickname unknown.") } url := fmt.Sprintf(site, username) resp, err := http.Get(url) // #nosec G107 -- URL is constructed from a hardcoded allowlist of sites fetchURL := fmt.Sprintf(site, username) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Get(fetchURL) // #nosec G107 -- URL is constructed from a hardcoded allowlist of sites if err != nil { return fmt.Sprintf("ERROR: Failed to fetch ssh keys (%s).\n", err) } defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) const maxBody = 1024 * 1024 // 1 MB const maxKeys = 100 scanner := bufio.NewScanner(io.LimitReader(resp.Body, maxBody)) keys := 0 dups := 0 for scanner.Scan() { if keys+dups >= maxKeys { return fmt.Sprintf("ERROR: Too many keys (max %d).\n", maxKeys) } keyline := string(bytes.TrimSpace(scanner.Bytes())) if err := AddDB(q, strings.ToUpper(login), keyline); err != nil { if errors.Is(err, ErrDuplicateKey) { Loading repl/mail.go +7 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import ( "git.lyda.ie/pp/bulletin/ask" "git.lyda.ie/pp/bulletin/dclish" "git.lyda.ie/pp/bulletin/editor" "git.lyda.ie/pp/bulletin/folders" "git.lyda.ie/pp/bulletin/storage" "git.lyda.ie/pp/bulletin/this" "github.com/adrg/xdg" Loading Loading @@ -127,6 +128,9 @@ func ActionMail(cmd *dclish.Command) error { return err } if err := folders.ValidateMessageSize(subject, message); err != nil { return err } for _, recipient := range recipients { err = this.Q.CreateMail(ctx, storage.CreateMailParams{ FromLogin: this.User.Login, Loading Loading @@ -244,6 +248,9 @@ func ActionRespond(cmd *dclish.Command) error { } } if err := folders.ValidateMessageSize(subject, message); err != nil { return err } err = this.Q.CreateMail(ctx, storage.CreateMailParams{ FromLogin: this.User.Login, ToLogin: recipient, Loading repl/mail_actions.go +7 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ import ( "git.lyda.ie/pp/bulletin/ask" "git.lyda.ie/pp/bulletin/dclish" "git.lyda.ie/pp/bulletin/editor" "git.lyda.ie/pp/bulletin/folders" "git.lyda.ie/pp/bulletin/pager" "git.lyda.ie/pp/bulletin/storage" "git.lyda.ie/pp/bulletin/this" Loading Loading @@ -104,6 +105,9 @@ func ActionMailSend(cmd *dclish.Command) error { return err } if err := folders.ValidateMessageSize(subject, message); err != nil { return err } for _, recipient := range recipients { err = this.Q.CreateMail(ctx, storage.CreateMailParams{ FromLogin: this.User.Login, Loading Loading @@ -238,6 +242,9 @@ func ActionMailReply(_ *dclish.Command) error { return err } if err := folders.ValidateMessageSize(subject, message); err != nil { return err } err = this.Q.CreateMail(ctx, storage.CreateMailParams{ FromLogin: this.User.Login, ToLogin: mail.FromLogin, Loading repl/messages.go +48 −17 Original line number Diff line number Diff line Loading @@ -67,23 +67,56 @@ func ActionDirectory(cmd *dclish.Command) error { } // Get base message list based on filter flags. ctx := storage.Context() needBody := cmd.Flags["/PRINT"].Value == "true" var msgs []storage.Message var err error switch { case cmd.Flags["/ALL"].Set: if needBody { msgs, err = folders.ListMessages(this.Folder.Name) } else { msgs, err = folders.DirMessages(this.Folder.Name) } case cmd.Flags["/MARKED"].Set: if needBody { msgs, err = folders.ListMessagesMarked(this.User.Login, this.Folder.Name) } else { msgs, err = folders.DirMessagesMarked(this.User.Login, this.Folder.Name) } case cmd.Flags["/UNMARKED"].Set: if needBody { msgs, err = folders.ListMessagesUnmarked(this.User.Login, this.Folder.Name) } else { msgs, err = folders.DirMessagesUnmarked(this.User.Login, this.Folder.Name) } case cmd.Flags["/SEEN"].Set: if needBody { msgs, err = folders.ListMessagesSeen(this.User.Login, this.Folder.Name) } else { msgs, err = folders.DirMessagesSeen(this.User.Login, this.Folder.Name) } case cmd.Flags["/NEW"].Set || cmd.Flags["/UNSEEN"].Set: if needBody { msgs, err = folders.ListMessagesUnseen(this.User.Login, this.Folder.Name) } else { msgs, err = folders.DirMessagesUnseen(this.User.Login, this.Folder.Name) } case cmd.Flags["/SEARCH"].Set: if needBody { msgs, err = this.Q.Search(ctx, nullStr(cmd.Flags["/SEARCH"].Value), 1, this.Folder.Name) } else { var rows []storage.DirSearchRow rows, err = this.Q.DirSearch(ctx, nullStr(cmd.Flags["/SEARCH"].Value), 1, this.Folder.Name) msgs = folders.ToMessages(rows) } case cmd.Flags["/SUBJECT"].Set: if needBody { msgs, err = this.Q.SearchSubject(ctx, nullStr(cmd.Flags["/SUBJECT"].Value), 1, this.Folder.Name) } else { var rows []storage.DirSearchSubjectRow rows, err = this.Q.DirSearchSubject(ctx, nullStr(cmd.Flags["/SUBJECT"].Value), 1, this.Folder.Name) msgs = folders.ToMessages(rows) } case cmd.Flags["/REPLY"].Set: if this.MsgID == 0 { fmt.Println("No current message for /REPLY.") Loading @@ -94,17 +127,15 @@ func ActionDirectory(cmd *dclish.Command) error { return e } replySubj := "Re: " + curMsg.Subject all, e := folders.ListMessages(this.Folder.Name) if e != nil { return e } for _, m := range all { if strings.HasPrefix(m.Subject, replySubj) { msgs = append(msgs, m) } } var rows []storage.DirSearchReplyRow rows, err = this.Q.DirSearchReply(ctx, nullStr(replySubj), this.Folder.Name) msgs = folders.ToMessages(rows) default: if needBody { msgs, err = folders.ListMessages(this.Folder.Name) } else { msgs, err = folders.DirMessages(this.Folder.Name) } } if err != nil { return err Loading Loading
folders/messages.go +80 −0 Original line number Diff line number Diff line Loading @@ -2,14 +2,36 @@ package folders import ( "errors" "fmt" "time" "unsafe" "git.lyda.ie/pp/bulletin/storage" "git.lyda.ie/pp/bulletin/this" ) // MaxMessageSize is the maximum size of a message body in bytes (64 KB). const MaxMessageSize = 64 * 1024 // MaxSubjectSize is the maximum size of a subject in bytes. const MaxSubjectSize = 256 // ValidateMessageSize checks that a message and subject are within size limits. func ValidateMessageSize(subject, message string) error { if len(subject) > MaxSubjectSize { return fmt.Errorf("subject too long (%d bytes, max %d)", len(subject), MaxSubjectSize) } if len(message) > MaxMessageSize { return fmt.Errorf("message too long (%d bytes, max %d)", len(message), MaxMessageSize) } return nil } // CreateMessage creates a new message. func CreateMessage(author, subject, message, folder string, permanent, system, shutdown int, expiration *time.Time) error { if err := ValidateMessageSize(subject, message); err != nil { return err } ctx := storage.Context() if expiration == nil { days, err := this.Q.GetFolderExpire(ctx, folder) Loading Loading @@ -173,6 +195,64 @@ func ListMessages(folder string) ([]storage.Message, error) { return rows, err } // dirRow is the interface for Dir query row types that have the same // fields as Message. type dirRow interface { storage.DirMessagesRow | storage.DirMessagesSeenRow | storage.DirMessagesUnseenRow | storage.DirMessagesMarkedRow | storage.DirMessagesUnmarkedRow | storage.DirSearchRow | storage.DirSearchSubjectRow | storage.DirSearchReplyRow } // ToMessages converts Dir query rows to Messages. Exported for use in repl. func ToMessages[T dirRow](rows []T) []storage.Message { return toMessages(rows) } func toMessages[T dirRow](rows []T) []storage.Message { msgs := make([]storage.Message, len(rows)) for i := range rows { msgs[i] = *(*storage.Message)(unsafe.Pointer(&rows[i])) // #nosec G103 -- types are layout-identical } return msgs } // DirMessages lists messages without message bodies. func DirMessages(folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessages(ctx, folder) return toMessages(rows), err } // DirMessagesSeen lists seen messages without message bodies. func DirMessagesSeen(login, folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessagesSeen(ctx, folder, login) return toMessages(rows), err } // DirMessagesUnseen lists unseen messages without message bodies. func DirMessagesUnseen(login, folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessagesUnseen(ctx, folder, login) return toMessages(rows), err } // DirMessagesMarked lists marked messages without message bodies. func DirMessagesMarked(login, folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessagesMarked(ctx, folder, login) return toMessages(rows), err } // DirMessagesUnmarked lists unmarked messages without message bodies. func DirMessagesUnmarked(login, folder string) ([]storage.Message, error) { ctx := storage.Context() rows, err := this.Q.DirMessagesUnmarked(ctx, folder, login) return toMessages(rows), err } // WroteAllMessages returns true if login wrote all msgids func WroteAllMessages(login string, msgids []int64) bool { ctx := storage.Context() Loading
key/key.go +27 −9 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ import ( "os" "path" "strings" "time" "git.lyda.ie/pp/bulletin/storage" "github.com/adrg/xdg" Loading Loading @@ -60,14 +61,25 @@ func addToFile(login, public string) error { } defer unix.Flock(fd, unix.LOCK_UN) //nolint:errcheck // unlock after we're done // Check for duplicates. keycontent, err := io.ReadAll(f) // Check for duplicates using a scanner. newKeyBytes := theKey.Marshal() scanner := bufio.NewScanner(f) for scanner.Scan() { line := bytes.TrimSpace(scanner.Bytes()) if len(line) == 0 { continue } existing, _, _, _, err := ssh.ParseAuthorizedKey(line) if err != nil { return err continue } if bytes.Contains(keycontent, ssh.MarshalAuthorizedKey(theKey)) { if bytes.Equal(existing.Marshal(), newKeyBytes) { return ErrDuplicateKey } } if err := scanner.Err(); err != nil { return err } // Generate and write the key. keyline := fmt.Sprintf(keytemplate, bulletin, login, string(ssh.MarshalAuthorizedKey(theKey))) Loading Loading @@ -225,16 +237,22 @@ func FetchDB(q *storage.Queries, login, nickname, username string) string { if site == "" { return fmt.Sprintln("ERROR: site nickname unknown.") } url := fmt.Sprintf(site, username) resp, err := http.Get(url) // #nosec G107 -- URL is constructed from a hardcoded allowlist of sites fetchURL := fmt.Sprintf(site, username) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Get(fetchURL) // #nosec G107 -- URL is constructed from a hardcoded allowlist of sites if err != nil { return fmt.Sprintf("ERROR: Failed to fetch ssh keys (%s).\n", err) } defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) const maxBody = 1024 * 1024 // 1 MB const maxKeys = 100 scanner := bufio.NewScanner(io.LimitReader(resp.Body, maxBody)) keys := 0 dups := 0 for scanner.Scan() { if keys+dups >= maxKeys { return fmt.Sprintf("ERROR: Too many keys (max %d).\n", maxKeys) } keyline := string(bytes.TrimSpace(scanner.Bytes())) if err := AddDB(q, strings.ToUpper(login), keyline); err != nil { if errors.Is(err, ErrDuplicateKey) { Loading
repl/mail.go +7 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import ( "git.lyda.ie/pp/bulletin/ask" "git.lyda.ie/pp/bulletin/dclish" "git.lyda.ie/pp/bulletin/editor" "git.lyda.ie/pp/bulletin/folders" "git.lyda.ie/pp/bulletin/storage" "git.lyda.ie/pp/bulletin/this" "github.com/adrg/xdg" Loading Loading @@ -127,6 +128,9 @@ func ActionMail(cmd *dclish.Command) error { return err } if err := folders.ValidateMessageSize(subject, message); err != nil { return err } for _, recipient := range recipients { err = this.Q.CreateMail(ctx, storage.CreateMailParams{ FromLogin: this.User.Login, Loading Loading @@ -244,6 +248,9 @@ func ActionRespond(cmd *dclish.Command) error { } } if err := folders.ValidateMessageSize(subject, message); err != nil { return err } err = this.Q.CreateMail(ctx, storage.CreateMailParams{ FromLogin: this.User.Login, ToLogin: recipient, Loading
repl/mail_actions.go +7 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ import ( "git.lyda.ie/pp/bulletin/ask" "git.lyda.ie/pp/bulletin/dclish" "git.lyda.ie/pp/bulletin/editor" "git.lyda.ie/pp/bulletin/folders" "git.lyda.ie/pp/bulletin/pager" "git.lyda.ie/pp/bulletin/storage" "git.lyda.ie/pp/bulletin/this" Loading Loading @@ -104,6 +105,9 @@ func ActionMailSend(cmd *dclish.Command) error { return err } if err := folders.ValidateMessageSize(subject, message); err != nil { return err } for _, recipient := range recipients { err = this.Q.CreateMail(ctx, storage.CreateMailParams{ FromLogin: this.User.Login, Loading Loading @@ -238,6 +242,9 @@ func ActionMailReply(_ *dclish.Command) error { return err } if err := folders.ValidateMessageSize(subject, message); err != nil { return err } err = this.Q.CreateMail(ctx, storage.CreateMailParams{ FromLogin: this.User.Login, ToLogin: mail.FromLogin, Loading
repl/messages.go +48 −17 Original line number Diff line number Diff line Loading @@ -67,23 +67,56 @@ func ActionDirectory(cmd *dclish.Command) error { } // Get base message list based on filter flags. ctx := storage.Context() needBody := cmd.Flags["/PRINT"].Value == "true" var msgs []storage.Message var err error switch { case cmd.Flags["/ALL"].Set: if needBody { msgs, err = folders.ListMessages(this.Folder.Name) } else { msgs, err = folders.DirMessages(this.Folder.Name) } case cmd.Flags["/MARKED"].Set: if needBody { msgs, err = folders.ListMessagesMarked(this.User.Login, this.Folder.Name) } else { msgs, err = folders.DirMessagesMarked(this.User.Login, this.Folder.Name) } case cmd.Flags["/UNMARKED"].Set: if needBody { msgs, err = folders.ListMessagesUnmarked(this.User.Login, this.Folder.Name) } else { msgs, err = folders.DirMessagesUnmarked(this.User.Login, this.Folder.Name) } case cmd.Flags["/SEEN"].Set: if needBody { msgs, err = folders.ListMessagesSeen(this.User.Login, this.Folder.Name) } else { msgs, err = folders.DirMessagesSeen(this.User.Login, this.Folder.Name) } case cmd.Flags["/NEW"].Set || cmd.Flags["/UNSEEN"].Set: if needBody { msgs, err = folders.ListMessagesUnseen(this.User.Login, this.Folder.Name) } else { msgs, err = folders.DirMessagesUnseen(this.User.Login, this.Folder.Name) } case cmd.Flags["/SEARCH"].Set: if needBody { msgs, err = this.Q.Search(ctx, nullStr(cmd.Flags["/SEARCH"].Value), 1, this.Folder.Name) } else { var rows []storage.DirSearchRow rows, err = this.Q.DirSearch(ctx, nullStr(cmd.Flags["/SEARCH"].Value), 1, this.Folder.Name) msgs = folders.ToMessages(rows) } case cmd.Flags["/SUBJECT"].Set: if needBody { msgs, err = this.Q.SearchSubject(ctx, nullStr(cmd.Flags["/SUBJECT"].Value), 1, this.Folder.Name) } else { var rows []storage.DirSearchSubjectRow rows, err = this.Q.DirSearchSubject(ctx, nullStr(cmd.Flags["/SUBJECT"].Value), 1, this.Folder.Name) msgs = folders.ToMessages(rows) } case cmd.Flags["/REPLY"].Set: if this.MsgID == 0 { fmt.Println("No current message for /REPLY.") Loading @@ -94,17 +127,15 @@ func ActionDirectory(cmd *dclish.Command) error { return e } replySubj := "Re: " + curMsg.Subject all, e := folders.ListMessages(this.Folder.Name) if e != nil { return e } for _, m := range all { if strings.HasPrefix(m.Subject, replySubj) { msgs = append(msgs, m) } } var rows []storage.DirSearchReplyRow rows, err = this.Q.DirSearchReply(ctx, nullStr(replySubj), this.Folder.Name) msgs = folders.ToMessages(rows) default: if needBody { msgs, err = folders.ListMessages(this.Folder.Name) } else { msgs, err = folders.DirMessages(this.Folder.Name) } } if err != nil { return err Loading