Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/markmnl/fmsgd v0.2.2-0.20260516060107-d5ee903e07bf // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Comment thread
markmnl marked this conversation as resolved.
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzh
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/markmnl/fmsgd v0.2.2-0.20260516060107-d5ee903e07bf h1:xtX//kGqFoG2acJzS/KExMP3jwGt4J9QSO/hV7fNcaI=
github.com/markmnl/fmsgd v0.2.2-0.20260516060107-d5ee903e07bf/go.mod h1:DNykk/1Z25IM+yzSxjtJs51nEYYaA1H03dAMG37JERk=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
98 changes: 94 additions & 4 deletions src/handlers/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5"
"github.com/markmnl/fmsgd/pkg/fmsg"

"github.com/markmnl/fmsg-webapi/db"
"github.com/markmnl/fmsg-webapi/middleware"
Expand Down Expand Up @@ -806,7 +807,7 @@ func (h *MessageHandler) Send(c *gin.Context) {
}

ctx := c.Request.Context()
existing, _, err := h.fetchMessage(ctx, msgID)
existing, dataPath, err := h.fetchMessage(ctx, msgID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
Expand All @@ -826,8 +827,97 @@ func (h *MessageHandler) Send(c *gin.Context) {
return
}

// Fetch psha256 (the parent message's sha256 hash used as the pid wire field).
var psha256 []byte
if existing.HasPid {
_ = h.DB.Pool.QueryRow(ctx, "SELECT psha256 FROM msg WHERE id = $1", msgID).Scan(&psha256)
}
Comment on lines +830 to +843

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 18ccd92:

  • psha256 lookup now uses err = instead of _ =, returning 5xx on any DB error
  • Added a length check: if psha256 is not exactly 32 bytes, the handler returns 5xx with a clear error message
  • Attachment query errors now return 5xx instead of silently treating it as "no attachments"
  • Per-row scan errors during attachment iteration now close the rows and return 5xx
  • Added attRows.Err() check after the iteration loop


// Fetch attachment metadata needed for hash computation.
type attachMeta struct {
flags int16
typ string
filename string
filesize int32
path string
}
var attachments []attachMeta
attRows, attErr := h.DB.Pool.Query(ctx,
"SELECT flags, type, filename, filesize, filepath FROM msg_attachment WHERE msg_id = $1 ORDER BY position",
msgID,
)
if attErr == nil {
for attRows.Next() {
var a attachMeta
if scanErr := attRows.Scan(&a.flags, &a.typ, &a.filename, &a.filesize, &a.path); scanErr == nil {
attachments = append(attachments, a)
}
}
attRows.Close()
}

now := float64(time.Now().UnixMicro()) / 1e6
if _, err = h.DB.Pool.Exec(ctx, "UPDATE msg SET time_sent = $1 WHERE id = $2", now, msgID); err != nil {

// Build fmsg.Header for hash computation.
fromUser, fromDomain := parseAddr(existing.From)
hdr := fmsg.Header{
Version: uint8(existing.Version),
From: fmsg.Address{User: fromUser, Domain: fromDomain},
Timestamp: now,
Topic: existing.Topic,
Type: existing.Type,
Size: uint32(existing.Size),
Filepath: dataPath,
}

if existing.HasPid {
hdr.Flags |= fmsg.FlagHasPid
hdr.Pid = psha256
}
if existing.Important {
hdr.Flags |= fmsg.FlagImportant
}
if existing.NoReply {
hdr.Flags |= fmsg.FlagNoReply
}
if existing.HasAddTo {
hdr.Flags |= fmsg.FlagHasAddTo
for _, addr := range existing.AddTo {
u, d := parseAddr(addr)
hdr.AddTo = append(hdr.AddTo, fmsg.Address{User: u, Domain: d})
}
if existing.AddToFrom != nil {
u, d := parseAddr(*existing.AddToFrom)
hdr.AddToFrom = &fmsg.Address{User: u, Domain: d}
}
}

for _, addr := range existing.To {
u, d := parseAddr(addr)
hdr.To = append(hdr.To, fmsg.Address{User: u, Domain: d})
}

for _, a := range attachments {
hdr.Attachments = append(hdr.Attachments, fmsg.AttachmentHeader{
Flags: uint8(a.flags),
Type: a.typ,
Filename: a.filename,
Size: uint32(a.filesize),
Filepath: a.path,
})
}

msgHash, err := hdr.GetMessageHash()
if err != nil {
log.Printf("send message %d: compute hash: %v", msgID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to compute message hash"})
return
}

if _, err = h.DB.Pool.Exec(ctx,
"UPDATE msg SET time_sent = $1, sha256 = $2 WHERE id = $3",
now, msgHash, msgID,
); err != nil {
log.Printf("send message %d: %v", msgID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send message"})
return
Expand Down Expand Up @@ -995,15 +1085,15 @@ func (h *MessageHandler) AddRecipients(c *gin.Context) {
// after performing their own authorization checks.
func (h *MessageHandler) fetchMessage(ctx context.Context, msgID int64) (*models.Message, string, error) {
row := h.DB.Pool.QueryRow(ctx,
`SELECT version, pid, no_reply, is_important, is_deflate, time_sent, from_addr, topic, type, size, filepath FROM msg WHERE id = $1`,
`SELECT version, pid, no_reply, is_important, is_deflate, time_sent, from_addr, add_to_from, topic, type, size, filepath FROM msg WHERE id = $1`,
msgID,
)

msg := &models.Message{}
var pid *int64
var timeSent *float64
var dataPath string
if err := row.Scan(&msg.Version, &pid, &msg.NoReply, &msg.Important, &msg.Deflate, &timeSent, &msg.From, &msg.Topic, &msg.Type, &msg.Size, &dataPath); err != nil {
if err := row.Scan(&msg.Version, &pid, &msg.NoReply, &msg.Important, &msg.Deflate, &timeSent, &msg.From, &msg.AddToFrom, &msg.Topic, &msg.Type, &msg.Size, &dataPath); err != nil {
return nil, "", err
}
msg.PID = pid
Expand Down
Loading