mirror of
https://github.com/Dvorinka/Productier.git
synced 2026-06-04 20:43:02 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user