Skip to content

Commit

Permalink
Merge pull request #4 from Multidialogo/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
zio-mitch authored Jan 27, 2025
2 parents 7ec435b + f0725bd commit 10d0d42
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 84 deletions.
6 changes: 4 additions & 2 deletions .env.dev
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
APP_DATA_PATH="/var/lib/mailculator
DRAFT_OUTPUT_PATH="/email_queues/draft"
APP_DATA_PATH="/var/lib/mailculators"
INPUT_PATH="/input"
DRAFT_OUTPUT_PATH="/email_queues/draft"
OUTBOX_PATH="/maildir/outbox"

3 changes: 2 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
APP_DATA_PATH="/tmp"
DRAFT_OUTPUT_PATH="/email_queues/draft"
INPUT_PATH="/input"
DRAFT_OUTPUT_PATH="/email_queues/draft"
OUTBOX_PATH="/maildir/outbox"
33 changes: 17 additions & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
# Stage 1: Builder
FROM golang:1.23 AS builder
RUN mkdir -p /usr/local/go/src/mailculator
WORKDIR /usr/local/go/src/mailculator
FROM golang:1.23 AS mailculators-builder
RUN mkdir -p /usr/local/go/src/mailculator-server
WORKDIR /usr/local/go/src/mailculator-server
COPY . .
COPY .env.test /usr/local/go/src/mailculator/.env
COPY .env.test /usr/local/go/src/mailculator-server/.env
RUN go mod tidy
RUN go mod download
RUN go test ./...
RUN go build -o /usr/local/bin/mailculator .
RUN chmod +x /usr/local/bin/mailculator
RUN go build -o /usr/local/bin/mailculator-server/httpd .
RUN chmod +x /usr/local/bin/mailculator-server/httpd

# Stage 2: Development
FROM golang:1.23 AS dev
WORKDIR /usr/local/go/src/mailculator
COPY data /var/lib/mailculator
COPY --from=builder /usr/local/bin/mailculator /usr/local/bin/mailculator/server
COPY .env.dev /usr/local/bin/mailculator/.env
FROM golang:1.23 AS mailculators-dev
WORKDIR /usr/local/go/src/mailculator-server
COPY . .
COPY .env.dev /usr/local/go/src/mailculator-server/.env
RUN go mod tidy
RUN go mod download
RUN go install github.com/air-verse/air@latest
EXPOSE 8080
CMD ["air"]

# Stage 3: Production
FROM gcr.io/distroless/base-debian12 AS prod
WORKDIR /usr/local/go/src/mailculator
COPY --from=builder /usr/local/bin/mailculator /usr/local/bin/mailculator/server
COPY .env.prod /usr/local/bin/mailculator/.env
FROM gcr.io/distroless/base-debian12 AS mailculators-prod
WORKDIR /usr/local/bin/mailculator/server
COPY --from=mailculators-builder /usr/local/bin/mailculator-server/httpd /usr/local/bin/mailculator-server/httpd
COPY .env.prod /usr/local/bin/mailculator-server/.env
EXPOSE 8080
CMD ["mailcalculator"]
CMD ["httpd"]
25 changes: 10 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

# MultiDialogo - MailCulator
# MultiDialogo - MailCulator server

## Provisioning

This Dockerfile is designed to build and deploy the `mailculator` application using three distinct stages:
This Dockerfile is designed to build and deploy the `mailculator server` application using three distinct stages:
1. Builder Stage
2. Development Stage
3. Production Stage
Expand All @@ -17,20 +17,20 @@ This stage is responsible for building the Go application, running tests, and pr

Description:
- The base image used is `golang:1.23`.
- The `mailculator` project is copied into the container and the necessary dependencies are downloaded using `go mod tidy` and `go mod download`.
- The `mailculator server` project is copied into the container and the necessary dependencies are downloaded using `go mod tidy` and `go mod download`.
- The tests are run with `go test ./...` to ensure everything is correct.
- The application is built with `go build` and the resulting binary is copied to `/usr/local/bin/mailculator`.
- The application is built with `go build` and the resulting binary is copied to `/usr/local/bin/mailculator-server`.
- Finally, the binary is made executable with `chmod +x`.

To build the image:
```bash
docker build --no-cache -t mailculator-builder --target builder .
docker build --no-cache -t mailculators-builder --target mailculators-builder .
```

To introspect the builder image:

```bash
docker run -ti --rm mailculator-builder bash
docker run -ti --rm mailculators-builder bash
```

### Stage 2: Development
Expand All @@ -45,17 +45,17 @@ The air tool is installed, which enables live-reload for Go projects during deve
The container exposes port 8080, which can be used for development and debugging.
To build the image:
```bash
docker build -t mailculator-dev --target dev .
docker build -t mailculators-dev --target mailculators-dev .
```

To run the development container:
```bash
docker run -p 8080:8080 mailculator-dev
docker run -v$(pwd)/data:/var/lib/mailculator-server -p 8080:8080 mailculators-dev
```

### Stage 3: Production

Purpose: This stage is optimized for production deployment. It creates a minimal container to run the mailculator binary in a secure and efficient environment.
Purpose: This stage is optimized for production deployment. It creates a minimal container to run the mailculator server binary in a secure and efficient environment.

Description:

Expand All @@ -65,10 +65,5 @@ Port 8080 is exposed for production use.
The container is configured to run the mailculator binary.
To build the image:
```bash
docker build -t mailculator-prod --target prod .
```

To run the production container:
```bash
docker run -p 8080:8080 mailculator-prod
docker build -t mailculators-prod --target mailculators-prod .
```
67 changes: 21 additions & 46 deletions internal/service/email_queue_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"fmt"
"mailculator/internal/model"
"mailculator/internal/utils"
"mime/multipart"
"mime/quotedprintable"
"net/mail"
Expand All @@ -12,30 +13,31 @@ import (
"io/ioutil"
"encoding/base64"
"net/textproto"
"github.com/h2non/filetype"
)

type EmailQueueStorage struct {
DraftOutputPath string
OutboxPath string
}

func NewEmailQueueStorage(draftOutputPath string) *EmailQueueStorage {
return &EmailQueueStorage{DraftOutputPath: draftOutputPath}
func NewEmailQueueStorage(draftOutputPath string, outboxPath string) *EmailQueueStorage {
return &EmailQueueStorage{DraftOutputPath: draftOutputPath, OutboxPath: outboxPath}
}

func (s *EmailQueueStorage) SaveEmailsAsEML(emails []*model.Email) error {
var filePaths []string
for _, email := range emails {
// Generate file path for the .EML file
filePath := filepath.Join(s.DraftOutputPath, fmt.Sprintf("%s.EML", email.Path()))

draftPath := filepath.Join(s.DraftOutputPath, fmt.Sprintf("%s.EML", email.Path()))
filePaths = append(filePaths, fmt.Sprintf("%s.EML", email.Path()))
// Ensure the directory structure exists
dirPath := filepath.Dir(filePath)
dirPath := filepath.Dir(draftPath)
if err := os.MkdirAll(dirPath, 0755); err != nil {
return fmt.Errorf("failed to create directories for EML file: %w", err)
}

// Open the file for writing
file, err := os.Create(filePath)
file, err := os.Create(draftPath)
if err != nil {
return fmt.Errorf("failed to create EML file: %w", err)
}
Expand Down Expand Up @@ -120,6 +122,17 @@ func (s *EmailQueueStorage) SaveEmailsAsEML(emails []*model.Email) error {
}
}

// Copy created files in outbox directory
for _, fileToCopyPath := range filePaths {
var originalFilePath string = filepath.Join(s.DraftOutputPath, fileToCopyPath)
var destinationFilePath string = filepath.Join(s.OutboxPath, fileToCopyPath)
err := utils.CopyFile(originalFilePath, destinationFilePath)
if err != nil {
return fmt.Errorf("failed to move file from %s to %s: %w", originalFilePath, destinationFilePath, err)
}
return nil
}

return nil
}

Expand Down Expand Up @@ -147,7 +160,7 @@ func writePart(multipartWriter *multipart.Writer, contentType, charset, body str

// Helper function to write an attachment part
func writeAttachment(multipartWriter *multipart.Writer, path string, data []byte) error {
mimeType, err := detectFileMime(path)
mimeType, err := utils.DetectFileMime(path)
if err != nil {
return fmt.Errorf("failed to detect file mime type: %w", err)
}
Expand All @@ -174,44 +187,6 @@ func writeAttachment(multipartWriter *multipart.Writer, path string, data []byte
return nil
}

// detectFileMime detects the MIME type of a file using file signature and extension
func detectFileMime(path string) (string, error) {
// Open the file
file, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("Error opening file: %w", err)
}
defer file.Close()

// Read the first few bytes to detect content type using filetype
buffer := make([]byte, 261) // Read first 261 bytes, larger buffer for better detection
_, err = file.Read(buffer)
if err != nil {
return "", fmt.Errorf("Error reading file: %w", err)
}

// Use the filetype package to detect the MIME type based on file signature
if kind, _ := filetype.Match(buffer); kind != filetype.Unknown {
return kind.MIME.Value, nil
}

// Fallback to extension-based detection for known types
ext := filepath.Ext(path)
switch ext {
case ".jpg", ".jpeg":
return "image/jpeg", nil
case ".png":
return "image/png", nil
case ".gif":
return "image/gif", nil
case ".txt":
return "text/plain", nil
}

// Return application/octet-stream if no MIME type was found
return "application/octet-stream", nil
}

func isHeaderInList(slice []string, item string) bool {
for _, element := range slice {
if element == item {
Expand Down
9 changes: 7 additions & 2 deletions internal/service/email_queue_storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ func TestEmailQueueStorage_SaveEmailsAsEML(t *testing.T) {
basePath := t.TempDir() // t.TempDir() automatically creates a temp directory
require.NotEmpty(t, basePath, "Temp dir should not be empty")

draftPath := filepath.Join(basePath, "draft")

// Initialize EmailQueueStorage with the base path for storing EML files
emailQueueStorage := NewEmailQueueStorage(basePath)
emailQueueStorage := NewEmailQueueStorage(
draftPath,
filepath.Join(basePath, "outbox"),
)

// Define the test cases (data provider)
tests := []struct {
Expand Down Expand Up @@ -94,7 +99,7 @@ func TestEmailQueueStorage_SaveEmailsAsEML(t *testing.T) {
require.NoError(t, err, "Failed to save email as EML")

// Verify that the EML file was created
actualEmlFilePath := filepath.Join(basePath, tt.expectedEMLPath)
actualEmlFilePath := filepath.Join(draftPath, tt.expectedEMLPath)
_, err = os.Stat(actualEmlFilePath)
require.NoError(t, err, "EML file was not created")

Expand Down
90 changes: 90 additions & 0 deletions internal/utils/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package utils

import (
"fmt"
"github.com/h2non/filetype"
"os"
"path/filepath"
"strings"
"io"
)

// DetectFileMime detects the MIME type of a file using file signature and extension
func DetectFileMime(path string) (string, error) {
// Open the file
file, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("Error opening file: %w", err)
}
defer file.Close()

// Read the first few bytes to detect content type using filetype
buffer := make([]byte, 261) // Read first 261 bytes, larger buffer for better detection
_, err = file.Read(buffer)
if err != nil {
return "", fmt.Errorf("Error reading file: %w", err)
}

// Use the filetype package to detect the MIME type based on file signature
kind, _ := filetype.Match(buffer)
if kind == filetype.Unknown || kind.MIME.Value == "application/octet-stream" {
// Fallback to extension-based detection for known types
return DetectFileMimeFromKnownExtension(filepath.Ext(path)), nil
}

// If MIME type is found via file signature, return it
return kind.MIME.Value, nil
}

func DetectFileMimeFromKnownExtension(extension string) string {
switch strings.ToLower(extension) {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".gif":
return "image/gif"
case ".txt":
return "text/plain"
default:
return "application/octet-stream"
}
}

// CopyFile copies a file from src to dest, ensuring the destination path exists
func CopyFile(src, dest string) error {
// Ensure the destination directory exists
destDir := filepath.Dir(dest)
if err := os.MkdirAll(destDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}

// Open the source file
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source file: %w", err)
}
defer srcFile.Close()

// Create the destination file
destFile, err := os.Create(dest)
if err != nil {
return fmt.Errorf("failed to create destination file: %w", err)
}
defer destFile.Close()

// Copy the contents of the source file to the destination file
if _, err := io.Copy(destFile, srcFile); err != nil {
return fmt.Errorf("failed to copy file contents: %w", err)
}

// Optionally, set the destination file permissions to match the source file
srcInfo, err := os.Stat(src)
if err == nil {
if err := os.Chmod(dest, srcInfo.Mode()); err != nil {
return fmt.Errorf("failed to set file permissions: %w", err)
}
}

return nil
}
Loading

0 comments on commit 10d0d42

Please sign in to comment.