Files
Tomas Dvorak 3cb40adb23 first commit
2026-04-10 12:04:09 +02:00

230 lines
7.2 KiB
Go

package httpapi
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"productier/apps/backend/internal/mailruntime"
"productier/apps/backend/internal/store"
)
type connectMailboxRequest struct {
WorkspaceSlug string `json:"workspaceSlug" binding:"required"`
Label string `json:"label"`
Email string `json:"email" binding:"required,email"`
DisplayName string `json:"displayName"`
IMAPHost string `json:"imapHost" binding:"required"`
IMAPPort int `json:"imapPort"`
IMAPUsername string `json:"imapUsername"`
IMAPPassword string `json:"imapPassword" binding:"required"`
IMAPUseTLS bool `json:"imapUseTls"`
SMTPHost string `json:"smtpHost" binding:"required"`
SMTPPort int `json:"smtpPort"`
SMTPUsername string `json:"smtpUsername"`
SMTPPassword string `json:"smtpPassword"`
SMTPUseTLS bool `json:"smtpUseTls"`
}
type createOutgoingMailRequest struct {
WorkspaceSlug string `json:"workspaceSlug" binding:"required"`
MailboxID string `json:"mailboxId" binding:"required"`
To []store.MailAddress `json:"to" binding:"required"`
Cc []store.MailAddress `json:"cc"`
Bcc []store.MailAddress `json:"bcc"`
Subject string `json:"subject"`
TextBody string `json:"textBody"`
HTMLBody string `json:"htmlBody"`
ScheduledFor *time.Time `json:"scheduledFor"`
}
type createTaskFromMailRequest struct {
BoardGroupID string `json:"boardGroupId" binding:"required"`
Title string `json:"title"`
DueAt *time.Time `json:"dueAt"`
Color string `json:"color"`
}
func (s *Server) registerMailRoutes(group *gin.RouterGroup) {
group.GET("/mailboxes", func(c *gin.Context) {
workspaceSlug := c.Query("workspaceSlug")
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
return
}
c.JSON(http.StatusOK, gin.H{"data": s.store.ListMailboxes(workspaceSlug)})
})
group.POST("/mailboxes", func(c *gin.Context) {
var input connectMailboxRequest
if err := c.ShouldBindJSON(&input); err != nil {
s.writeStatusError(c, http.StatusBadRequest, err.Error())
return
}
if _, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug); !ok {
return
}
mailbox, err := s.mail.ConnectMailbox(c.Request.Context(), mailruntime.ConnectMailboxInput{
WorkspaceSlug: input.WorkspaceSlug,
Label: input.Label,
Email: input.Email,
DisplayName: input.DisplayName,
IMAPHost: input.IMAPHost,
IMAPPort: input.IMAPPort,
IMAPUsername: input.IMAPUsername,
IMAPPassword: input.IMAPPassword,
IMAPUseTLS: input.IMAPUseTLS,
SMTPHost: input.SMTPHost,
SMTPPort: input.SMTPPort,
SMTPUsername: input.SMTPUsername,
SMTPPassword: input.SMTPPassword,
SMTPUseTLS: input.SMTPUseTLS,
})
if err != nil {
s.writeStatusError(c, http.StatusBadRequest, err.Error())
return
}
c.JSON(http.StatusCreated, gin.H{"data": mailbox})
})
group.POST("/mailboxes/:mailboxId/sync", func(c *gin.Context) {
mailbox, err := s.store.GetMailboxByID(c.Param("mailboxId"))
if err != nil {
s.writeStatusError(c, http.StatusNotFound, err.Error())
return
}
if _, ok := s.requireWorkspaceMember(c, mailbox.WorkspaceSlug); !ok {
return
}
if err := s.mail.SyncMailbox(c.Request.Context(), mailbox.ID); err != nil {
s.writeStatusError(c, http.StatusBadGateway, err.Error())
return
}
updated, err := s.store.GetMailboxByID(mailbox.ID)
if err != nil {
s.writeStatusError(c, http.StatusNotFound, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"data": updated})
})
group.GET("/mail/messages", func(c *gin.Context) {
workspaceSlug := c.Query("workspaceSlug")
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
return
}
c.JSON(http.StatusOK, gin.H{"data": s.store.ListMailMessages(workspaceSlug, c.Query("mailboxId"))})
})
group.GET("/mail/outgoing", func(c *gin.Context) {
workspaceSlug := c.Query("workspaceSlug")
if _, ok := s.requireWorkspaceMember(c, workspaceSlug); !ok {
return
}
c.JSON(http.StatusOK, gin.H{"data": s.store.ListOutgoingMails(workspaceSlug, c.Query("mailboxId"))})
})
group.POST("/mail/outgoing", func(c *gin.Context) {
var input createOutgoingMailRequest
if err := c.ShouldBindJSON(&input); err != nil {
s.writeStatusError(c, http.StatusBadRequest, err.Error())
return
}
if _, ok := s.requireWorkspaceMember(c, input.WorkspaceSlug); !ok {
return
}
mailbox, err := s.store.GetMailboxByID(input.MailboxID)
if err != nil {
s.writeStatusError(c, http.StatusNotFound, err.Error())
return
}
if mailbox.WorkspaceSlug != input.WorkspaceSlug {
s.writeStatusError(c, http.StatusForbidden, "mailbox does not belong to workspace")
return
}
item, err := s.mail.QueueOutgoingMail(c.Request.Context(), mailruntime.QueueOutgoingMailInput{
WorkspaceSlug: input.WorkspaceSlug,
MailboxID: input.MailboxID,
To: input.To,
Cc: input.Cc,
Bcc: input.Bcc,
Subject: input.Subject,
TextBody: input.TextBody,
HTMLBody: input.HTMLBody,
ScheduledFor: input.ScheduledFor,
})
if err != nil {
s.writeStatusError(c, http.StatusBadGateway, err.Error())
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
})
group.POST("/mail/messages/:messageId/create-task", func(c *gin.Context) {
message, err := s.store.GetMailMessageByID(c.Param("messageId"))
if err != nil {
s.writeStatusError(c, http.StatusNotFound, err.Error())
return
}
if _, ok := s.requireWorkspaceMember(c, message.WorkspaceSlug); !ok {
return
}
if message.LinkedTaskID != nil {
s.writeStatusError(c, http.StatusConflict, "message already linked to a task")
return
}
var input createTaskFromMailRequest
if err := c.ShouldBindJSON(&input); err != nil {
s.writeStatusError(c, http.StatusBadRequest, err.Error())
return
}
description := mailTaskDescription(message)
task := s.store.CreateTask(store.CreateTaskInput{
WorkspaceSlug: message.WorkspaceSlug,
BoardGroupID: input.BoardGroupID,
Title: firstNonBlank(strings.TrimSpace(input.Title), strings.TrimSpace(message.Subject), "Follow up on email"),
Description: description,
DueAt: input.DueAt,
Color: withFallback(input.Color, "blue"),
})
if _, err := s.store.LinkMailMessageTask(message.ID, task.ID); err != nil {
s.writeStatusError(c, http.StatusInternalServerError, err.Error())
return
}
c.JSON(http.StatusCreated, gin.H{"data": task})
})
}
func mailTaskDescription(message store.MailMessage) string {
var builder strings.Builder
if message.From.Email != "" {
builder.WriteString(fmt.Sprintf("From: %s <%s>\n\n", firstNonBlank(message.From.Name, "Sender"), message.From.Email))
}
body := firstNonBlank(strings.TrimSpace(message.TextBody), strings.TrimSpace(message.Snippet))
builder.WriteString(body)
return strings.TrimSpace(builder.String())
}
func firstNonBlank(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func withFallback(value string, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return value
}