Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
94 changes: 57 additions & 37 deletions api/controllers/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package controllers

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"

"github.com/UTDNebula/nebula-api/api/schema"
"github.com/gin-gonic/gin"
Expand All @@ -26,6 +29,9 @@ import (
// @Failure 500 {object} schema.APIResponse[string] "A string describing the error"
// @Failure 400 {object} schema.APIResponse[string] "A string describing the error"
func SendEmail(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
defer cancel()

var req schema.EmailRequest

if err := c.ShouldBindJSON(&req); err != nil {
Expand All @@ -42,8 +48,8 @@ func SendEmail(c *gin.Context) {
return
}

if err := m.To(req.To); err != nil {
respond(c, http.StatusBadRequest, "invalid to address", err.Error())
if err := m.To(req.To...); err != nil {
respond(c, http.StatusBadRequest, "invalid to address(es)", err.Error())
return
}

Expand All @@ -58,7 +64,7 @@ func SendEmail(c *gin.Context) {
m.EmbedReader(emb.Name, bytes.NewReader(emb.Data), mail.WithFileContentID(emb.Name))
}

if err := client.DialAndSend(m); err != nil {
if err := client.DialAndSendWithContext(ctx, m); err != nil {
respond(c, http.StatusInternalServerError, "failed to send email", err.Error())
return
}
Expand All @@ -69,59 +75,73 @@ func SendEmail(c *gin.Context) {
// @Id QueueEmail
// @Router /email/queue [post]
// @Tags Internal
// @Description "Queue an email to be sent via SMTP. This route is restricted to only Nebula Labs internal Projects."
// @Description "Queue an email to be sent via SMTP. Multi-recipient emails will be queued as separate emails to avoid bypassing queueing system. This route is restricted to only Nebula Labs internal Projects."
// @Accept json
// @Produce json
// @Param request body schema.EmailRequest true "Email Request Body"
// @Param x-email-queue-key header string true "The internal email queue key"
// @Success 200 {object} schema.APIResponse[schema.EmailRequest] "Email Request Body with Queued Task Name"
// @Failure 500 {object} schema.APIResponse[string] "A string describing the error"
// @Failure 400 {object} schema.APIResponse[string] "A string describing the error"
// @Param request body schema.EmailRequest true "Email Request Body"
// @Param x-email-queue-key header string true "The internal email queue key"
// @Success 200 {object} schema.APIResponse[[]string] "The list of queued task names"
// @Failure 500 {object} schema.APIResponse[string] "A string describing the error"
// @Failure 400 {object} schema.APIResponse[string] "A string describing the error"
func QueueEmail(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
defer cancel()

// Request must be able to bind to email request
var emailReq schema.EmailRequest
if err := c.ShouldBindJSON(&emailReq); err != nil {
respond(c, http.StatusBadRequest, "invalid request payload", err.Error())
return
}

body, err := json.Marshal(emailReq)
if err != nil {
respond(c, http.StatusInternalServerError, "failed to serialize email request", err.Error())
return
}

client := c.MustGet("tasksClient").(*cloudtasks.Client)

queuePath := os.Getenv("GCLOUD_EMAIL_QUEUE_PATH")
queueUrl := os.Getenv("GCLOUD_EMAIL_QUEUE_URL")

// Build the Task payload.
// https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks
taskReq := &taskspb.CreateTaskRequest{
Parent: queuePath,
Task: &taskspb.Task{
MessageType: &taskspb.Task_HttpRequest{
HttpRequest: &taskspb.HttpRequest{
HttpMethod: taskspb.HttpMethod_POST,
Url: queueUrl,
Headers: map[string]string{
"x-email-send-key": os.Getenv("EMAIL_SEND_ROUTE_KEY"), // Must get from env bc queue only has x-email-queue-key header
"x-api-key": c.GetHeader("x-api-key"),
baseEmailReq := emailReq
baseEmailReq.To = []string{}

queuedTasks := []string{}

numOfRecipients := len(emailReq.To)
for i, to := range emailReq.To {
baseEmailReq.To = []string{to}

body, err := json.Marshal(baseEmailReq)
if err != nil {
respond(c, http.StatusInternalServerError, fmt.Sprintf("failed to serialize email request for recipient %s %d/%d", to, i, numOfRecipients), err.Error())
return
}

// Build the Task payload.
// // https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks
taskReq := &taskspb.CreateTaskRequest{
Parent: queuePath,
Task: &taskspb.Task{
MessageType: &taskspb.Task_HttpRequest{
HttpRequest: &taskspb.HttpRequest{
HttpMethod: taskspb.HttpMethod_POST,
Url: queueUrl,
Headers: map[string]string{
"x-email-send-key": os.Getenv("EMAIL_SEND_ROUTE_KEY"), // Must get from env bc queue only has x-email-queue-key header
"x-api-key": c.GetHeader("x-api-key"),
},
},
},
},
},
}
}

// Add a payload message if one is present.
taskReq.Task.GetHttpRequest().Body = []byte(body)
// Add a payload message if one is present.
taskReq.Task.GetHttpRequest().Body = []byte(body)

task, err := client.CreateTask(c.Request.Context(), taskReq)
if err != nil {
respond(c, http.StatusInternalServerError, "failed to queue email", err.Error())
return
task, err := client.CreateTask(ctx, taskReq)
if err != nil {
respond(c, http.StatusInternalServerError, fmt.Sprintf("failed to queue email for recipient %s %d/%d", to, i, numOfRecipients), err.Error())
return
}

queuedTasks = append(queuedTasks, task.GetName())
}

respond(c, http.StatusOK, "success", task.GetName())
respond(c, http.StatusOK, "success", queuedTasks)
}
28 changes: 24 additions & 4 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,7 @@ const docTemplate = `{
},
"/email/queue": {
"post": {
"description": "\"Queue an email to be sent via SMTP. This route is restricted to only Nebula Labs internal Projects.\"",
"description": "\"Queue an email to be sent via SMTP. Multi-recipient emails will be queued as separate emails to avoid bypassing queueing system. This route is restricted to only Nebula Labs internal Projects.\"",
"consumes": [
"application/json"
],
Expand Down Expand Up @@ -1070,9 +1070,9 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "Email Request Body with Queued Task Name",
"description": "The list of queued task names",
"schema": {
"$ref": "#/definitions/schema.APIResponse-schema_EmailRequest"
"$ref": "#/definitions/schema.APIResponse-array_string"
}
},
"400": {
Expand Down Expand Up @@ -3442,6 +3442,23 @@ const docTemplate = `{
}
}
},
"schema.APIResponse-array_string": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "string"
}
},
"message": {
"type": "string"
},
"status": {
"type": "integer"
}
}
},
"schema.APIResponse-int": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -4133,7 +4150,10 @@ const docTemplate = `{
"type": "string"
},
"to": {
"type": "string"
"type": "array",
"items": {
"type": "string"
}
}
}
},
Expand Down
24 changes: 19 additions & 5 deletions api/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ definitions:
status:
type: integer
type: object
schema.APIResponse-array_string:
properties:
data:
items:
type: string
type: array
message:
type: string
status:
type: integer
type: object
schema.APIResponse-int:
properties:
data:
Expand Down Expand Up @@ -556,7 +567,9 @@ definitions:
subject:
type: string
to:
type: string
items:
type: string
type: array
required:
- body
- subject
Expand Down Expand Up @@ -1666,8 +1679,9 @@ paths:
post:
consumes:
- application/json
description: '"Queue an email to be sent via SMTP. This route is restricted
to only Nebula Labs internal Projects."'
description: '"Queue an email to be sent via SMTP. Multi-recipient emails will
be queued as separate emails to avoid bypassing queueing system. This route
is restricted to only Nebula Labs internal Projects."'
operationId: QueueEmail
parameters:
- description: Email Request Body
Expand All @@ -1685,9 +1699,9 @@ paths:
- application/json
responses:
"200":
description: Email Request Body with Queued Task Name
description: The list of queued task names
schema:
$ref: '#/definitions/schema.APIResponse-schema_EmailRequest'
$ref: '#/definitions/schema.APIResponse-array_string'
"400":
description: A string describing the error
schema:
Expand Down
2 changes: 1 addition & 1 deletion api/schema/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ type EmailAttachment struct {

type EmailRequest struct {
From string `json:"from,omitempty"`
To string `json:"to" binding:"required,email"`
To []string `json:"to" binding:"required,dive,email"`
Subject string `json:"subject" binding:"required"`
Body string `json:"body" binding:"required"`
Attachments []EmailAttachment `json:"attachments,omitempty"`
Expand Down
Loading