mirror of
https://github.com/Dvorinka/Productier.git
synced 2026-06-03 20:13:01 +00:00
230 lines
7.2 KiB
Go
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
|
|
}
|