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
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
version: "2"
run:
timeout: 2m
linters:
Expand Down
7 changes: 6 additions & 1 deletion docstore/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/luthersystems/svc/docstore"
"github.com/sirupsen/logrus"
)

type missingRetryer struct {
Expand Down Expand Up @@ -133,7 +134,11 @@ func (a *Store) GetStreaming(key string, w http.ResponseWriter) error {
w.Header().Set("Connection", "close")
w.Header().Set("Content-Type", *(result.ContentType))
w.Header().Set("Content-Length", fmt.Sprintf("%d", *(result.ContentLength)))
defer result.Body.Close()
defer func() {
if err := result.Body.Close(); err != nil {
logrus.WithError(err).Warn("get streaming: close")
}
}()
_, err := io.Copy(w, result.Body)
if err != nil {
return fmt.Errorf("s3 get: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.36.5
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down Expand Up @@ -91,4 +90,5 @@ require (
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
69 changes: 68 additions & 1 deletion mailer/mailer.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// Copyright © 2021 Luther Systems, Ltd. All right reserved.
// Copyright © 2025 Luther Systems, Ltd. All right reserved.

package mailer

import (
"bytes"
"context"
"fmt"
"mime/multipart"
"net/textproto"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
Expand Down Expand Up @@ -67,3 +71,66 @@ func (m *SES) Send(ctx context.Context, content string, email string, subject st
}
return nil
}

// Attachment represents a file to attach to the email.
type Attachment struct {
Filename string
Data []byte
}

// SendWithAttachment sends an email with one or more attachments.
func (m *SES) SendWithAttachment(ctx context.Context, body, to, subject string, attachments []Attachment) error {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)

// Set email headers
mimeHeaders := make(map[string]string)
mimeHeaders["From"] = m.sender
mimeHeaders["To"] = to
mimeHeaders["Subject"] = subject
mimeHeaders["MIME-Version"] = "1.0"
mimeHeaders["Content-Type"] = "multipart/mixed; boundary=" + writer.Boundary()

// Write email headers
var msg bytes.Buffer
for k, v := range mimeHeaders {
msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
}
msg.WriteString("\r\n")

// Write HTML body part
bodyWriter, _ := writer.CreatePart(textproto.MIMEHeader{
"Content-Type": {"text/html; charset=utf-8"},
})
if _, err := bodyWriter.Write([]byte(body)); err != nil {
return fmt.Errorf("write: %w", err)
}

// Attach files
for _, att := range attachments {
partHeader := textproto.MIMEHeader{}
partHeader.Set("Content-Type", "application/zip")
partHeader.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, att.Filename))
part, _ := writer.CreatePart(partHeader)
if _, err := part.Write(att.Data); err != nil {
return fmt.Errorf("write: %w", err)
}
}

if err := writer.Close(); err != nil {
return fmt.Errorf("close: %w", err)
}

if _, err := msg.Write(buf.Bytes()); err != nil {
return fmt.Errorf("write: %w", err)
}

input := &ses.SendRawEmailInput{
RawMessage: &ses.RawMessage{
Data: msg.Bytes(),
},
}

_, err := m.svc.SendRawEmailWithContext(ctx, input)
return err
}
47 changes: 46 additions & 1 deletion mailer/mailer_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright © 2021 Luther Systems, Ltd. All right reserved.
// Copyright © 2025 Luther Systems, Ltd. All right reserved.

package mailer

import (
"archive/zip"
"bytes"
"context"
"os"
"testing"
Expand Down Expand Up @@ -59,3 +61,46 @@ func TestSend(t *testing.T) {
}
t.Logf("Sent email to: %s", recipient)
}

// TestSendWithAttachment sends an email with a zip attachment.
func TestSendWithAttachment(t *testing.T) {
if os.Getenv("MAILER_SES_TESTS") == "" {
t.Skip("Skipping test: $MAILER_SES_TESTS not set")
}
recipient := DefaultSuccessEmail
if os.Getenv("MAILER_SES_RECIPIENT") != "" {
recipient = os.Getenv("MAILER_SES_RECIPIENT")
}
mailer, err := NewSES(SESRegion, EmailSender)
if err != nil {
t.Fatalf("init mailer: %v", err)
}
ctx, done := context.WithTimeout(context.Background(), reqTimeout)
defer done()

// Create a zip archive containing a file with "hello world"
var buf bytes.Buffer
zipWriter := zip.NewWriter(&buf)
fileWriter, err := zipWriter.Create("hello.txt")
if err != nil {
t.Fatalf("create zip entry: %v", err)
}
_, err = fileWriter.Write([]byte("hello world"))
if err != nil {
t.Fatalf("write zip entry: %v", err)
}
if err := zipWriter.Close(); err != nil {
t.Fatalf("close zip writer: %v", err)
}

attachment := Attachment{
Filename: "testdata.zip",
Data: buf.Bytes(),
}

err = mailer.SendWithAttachment(ctx, HTMLTemplateText, recipient, SubjectTemplateText+" With Attachment", []Attachment{attachment})
if err != nil {
t.Fatalf("send mailer with attachment: %v", err)
}
t.Logf("Sent email with attachment to: %s", recipient)
}
12 changes: 10 additions & 2 deletions midware/interface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ func testRequest(t *testing.T, server *httptest.Server, method string, rpath str
}
resp, err := (&http.Client{}).Do(r)
require.NoError(t, err, "request failure")
defer resp.Body.Close()
defer func() {
if err := resp.Body.Close(); err != nil {
require.NoError(t, err, "close")
}
}()
b, err := io.ReadAll(resp.Body)
require.NoError(t, err, "unable to read response")
return b
Expand All @@ -112,7 +116,11 @@ func testResponseHeaders(t *testing.T, server *httptest.Server, method string, r
}
resp, err := (&http.Client{}).Do(r)
require.NoError(t, err, "request failure")
defer resp.Body.Close()
defer func() {
if err := resp.Body.Close(); err != nil {
require.NoError(t, err, "close")
}
}()
return resp
}

Expand Down
2 changes: 2 additions & 0 deletions oracle/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ type Config struct {
publicContentHandlers *http.ServeMux
// publicContentPathPrefix configures endpoint to serve public content.
publicContentPathPrefix string
// DiscardUnknown will ignore unknown fields (not throw an error).
DiscardUnknown bool `yaml:"discard-unknown"`
}

const (
Expand Down
4 changes: 3 additions & 1 deletion oracle/deptx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ func TestOracleDepTx(t *testing.T) {
var out2 hellov1.UseDepTxResponse
err = json.NewDecoder(secondResp.Body).Decode(&out2)
require.NoError(t, err)
secondResp.Body.Close()
if err := secondResp.Body.Close(); err != nil {
require.NoError(t, err)
}

t.Logf("Second call: old=%s new=%s", out2.GetOldTxId(), out2.GetNewTxId())
require.Equal(t, out1.GetNewTxId(), out2.GetOldTxId(), "the oldTx should match the first call's newTx")
Expand Down
5 changes: 5 additions & 0 deletions oracle/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,18 @@ func (orc *Oracle) phylumHealthCheck(ctx context.Context) []*healthcheck.HealthC
Status: "DOWN",
}}
}
found := false
reports := ccHealth.GetReports()
for _, report := range reports {
if strings.EqualFold(report.GetServiceName(), orc.cfg.PhylumServiceName) {
orc.setPhylumVersion(report.GetServiceVersion())
found = true
break
}
}
if !found {
orc.Log(ctx).WithField("service_name", orc.cfg.PhylumServiceName).Debug("no phylum version found")
}
return reports
}

Expand Down
6 changes: 5 additions & 1 deletion oracle/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ func TestCookieAndHeaderForwarders(t *testing.T) {
reqBody := bytes.NewBufferString(`{"name": "Bob"}`)
resp, err := http.Post("http://"+gwLis.Addr().String()+"/v1/hello", "application/json", reqBody)
require.NoError(t, err)
defer resp.Body.Close()
defer func() {
if err := resp.Body.Close(); err != nil {
require.NoError(t, err)
}
}()

// Should be 200 OK
require.Equal(t, http.StatusOK, resp.StatusCode)
Expand Down
2 changes: 1 addition & 1 deletion oracle/oraclerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (orc *Oracle) grpcGatewayMux() *runtime.ServeMux {
UseProtoNames: true,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: false,
DiscardUnknown: orc.cfg.DiscardUnknown,
},
}),
}
Expand Down
6 changes: 5 additions & 1 deletion oracle/oracletester.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ func getFreeAddr() (string, error) {
if err != nil {
return "", fmt.Errorf("failed to get a free port: %w", err)
}
defer l.Close() // Close immediately so it can be reused
defer func() {
if err := l.Close(); err != nil { // Close immediately so it can be reused
logrus.WithError(err).Warn("getFreeAddr: close")
}
}()
return l.Addr().String(), nil
}

Expand Down
Loading